Issue
A middleware function getSession(request, opts): void retrieves a session from the database and attaches it to the request, using some opts.
- If the route is not protected, it will return early;
 - If the route is protected and no 
accountis stored for thesession, it will redirect to the /home route; - If the route is protected and no 
profileis stored for thesession, it will redirect to the /create-profile route. 
Thus, a request.session will have either:
- No 
account, - An 
account, or - An 
accountand aprofile. 
Problem:
How can I infer the type of request.session by the request and opts provided to  getSession()?
Example:
The current types, implementation and usage of getSession() are provided below.
// utils/types.ts
interface Session {
  id: number;
  // ...
  account: Account | null;
}
interface Account {
  id: number;
  // ...
  profile: Profile | null;
}
interface Profile {
  id: number;
  // ...
}
interface HttpRequest extends Request {
  session: Session;
}
// utils/session.ts
const getSession = async (request: HttpRequest, opts: { protected?: boolean } = {}) => {
  // Set request.session as session retrieved from database
  // EXAMPLE: session with an account and no profile
  request.session = { id: 1, account: { id: 1, profile: null } };
  // If route is not protected: return early
  if (!opts.protected) {
    return;
  }
  // If route is protected and there is no account: redirect to /home route
  if (!request.session.account) {
    throw new Response(null, { status: 302, headers: { Location: "/home" } });
  }
  // If route is protected and there is no profile: redirect to /create-profile route
  if (!request.session.account?.profile) {
    throw new Response(null, { status: 302, headers: { Location: "/create-profile" } });
  }
};
// routes/create-profile.tsx
const loader = async (request: HttpRequest) => {
  try {
    await getSession(request, { protected: true });
    // TODO:
    // Infer if the request.session has an account or profile after calling getSession()
    // EXAMPLE:
    // If route is protected and no redirect to /home page:
    // Infer that there is an account, i.e. request.session.account is not null
    const account = request.session.account;
    return null;
  } catch (error) {
    return error;
  }
};
                            Solution
TL;DR this is not currently possible as asked. You could work around it by having getSession() return its input as a narrowed type and have the caller use the return value.
You would like getSession(request, options) to act as an assertion function (at least when options.protected is true) that narrows the apparent type of request from HttpRequest to a type where request.session.account.profile is known to be defined.  That is, from HttpRequest to the equivalent of HttpRequest & {session: {account: {profile: Profile}}}.
If getSession() were a synchronous function, you could give it the following call signatures:
declare function getSessionSync(
  request: HttpRequest, opts: { protected: true }
): asserts request is HttpRequest & { session: { account: { profile: Profile } } };
declare function getSessionSync(
  request: HttpRequest, opts: { protected?: false }
): void;
And see it work as desired:
(request: HttpRequest) => {
  try {
    getSessionSync(request, { protected: true });
    const account = request.session.account;
    // const account: Account & { profile: Profile; }
    account.profile.id; // okay
    return null;
  } catch (error) {
    return error;
  }
};
Unfortunately, getSession() is an async function and TypeScript does not currently support async assertion functions as of TypeScript 4.9.  There is an open feature request for this at microsoft/TypeScript#37681, so perhaps in some future version of TypeScript you'll be able to write
// NOT VALID TS, DO NOT TRY THIS
declare function getSession(
  request: HttpRequest, opts: { protected: true }
): Promise<asserts request is HttpRequest & {session: {account: {profile: Profile}}}>;
But for now you can't, and you'll need to work around it.
One workaround is to do what we had to do before assertion functions were introduced to the language: instead of trying to make the function narrow the type of its argument, have it return the argument as the narrowed type... and then use the returned value afterward instead of the argument. Generally, instead of:
declare function f(x: A): asserts x is B;
declare const x: A;
acceptB(x); // error, x is not known to be B here
f(x);
acceptB(x); // okay, x has been narrowed to B
You could write:
declare function f(x: A): B;
declare const x: A;
acceptB(x); // error, x is not known to be B
const newX = f(x); // save result to a new variable
// use newX instead of x after this point
acceptB(newX); // okay, newX is known to be B
For your example code, this looks like:
declare function getSession(
  request: HttpRequest, opts: { protected: true }
): Promise<HttpRequest & { session: { account: { profile: Profile } } }>;
declare function getSession(
  request: HttpRequest, opts: { protected?: false }
): Promise<void>;
const loader = async (request: HttpRequest) => {
  try {
    const _request = await getSession(request, { protected: true });
    const account = _request.session.account;
    account.profile.id; // okay
    return null;
  } catch (error) {
    return error;
  }
};
This is clunkier than using an assertion function, but at least it works!
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.