bencondon
bencondon

Reputation: 35

Infer the variable type from function parameters

A middleware function getSession(request, opts): void retrieves a session from the database and attaches it to the request, using some opts.

Thus, a request.session will have either:

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;
  }
};

Upvotes: 1

Views: 83

Answers (2)

jcalz
jcalz

Reputation: 330171

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!


Playground link to code

Upvotes: 1

Marat
Marat

Reputation: 629

So, basically your request goes through validation in getSession function and after it is validated you assume that it has specific properties. You can just create 2 types, like Session (where needed properties still might be undefined) and ValidSession (where you made sure that needed properties exist), make HttpRequest generic like

interface HttpRequest<SessionType> extends Request {
  session: SessionType
}

and make it the return type of your validation function. At the end you will have a function that receives HttpRequest, with possibly undefined properties, but returns HttpRequest with all properties that you need.

const validateRequest = (request: HttpRequest<Session>): HttpRequest<ValidSession> => { ... }

Upvotes: 0

Related Questions