Reputation: 35
A middleware function getSession(request, opts): void
retrieves a session
from the database and attaches it to the request
, using some opts
.
account
is stored for the session
, it will redirect to the /home route;profile
is stored for the session
, it will redirect to the /create-profile route.Thus, a request.session
will have either:
account
,account
, oraccount
and a profile
.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
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!
Upvotes: 1
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