Determine the data type of a variable based on the parameters of a function

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

  • If the route is not secure, the function will exit early;
  • If the route is secure and no user account is associated with the session, it will redirect to the /home page;
  • If the route is secure and no user profile is linked to the session, it will redirect to the /create-profile page.

As a consequence, the request.session can have one of three states:

  • No user account,
  • A user account, or
  • A user account and a user profile.

Question:

How can I determine the type of request.session based on the input request and opts in getSession()?

Example:

The current types, implementation, and usage details for getSession() are outlined 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 to session fetched from database
  // EXAMPLE: session with an account and no profile
  request.session = { id: 1, account: { id: 1, profile: null } };

  // If route is not secure: exit early
  if (!opts.protected) {
    return;
  }

  // If route is secure and there is no account: redirect to /home page
  if (!request.session.account) {
    throw new Response(null, { status: 302, headers: { Location: "/home" } });
  }

  // If route is secure and there is no profile: redirect to /create-profile page
  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:
    // Determine if the request.session has an account or profile after calling getSession()
    // EXAMPLE:
    // If route is secure and no redirection to /home page:
    // Assume that there is an account, i.e. request.session.account is not null
    const account = request.session.account;

    return null;
  } catch (error) {
    return error;
  }
};

Answer №1

Summarized, the current request cannot be handled as requested. One way around this limitation is to modify getSession() to return its input in a narrowed type, and then have the caller use the returned value.


Your ideal scenario is for getSession(request, options) to function as an assertion function, particularly when options.protected equals true, narrowing down the apparent type of request from HttpRequest to a type where request.session.account.profile is certain to be defined. Essentially, transitioning from HttpRequest to

HttpRequest & {session: {account: {profile: Profile}}}
.

If getSession() operated synchronously, you could establish 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;

This setup would work as anticipated:

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

Regrettably, getSession() functions asynchronously and TypeScript doesn't currently support async assertion functions as of TypeScript 4.9. There is a feature request pending at microsoft/TypeScript#37681, so perhaps future TypeScript iterations will enable scripting like this:

// NOT VALID TS, DO NOT TRY THIS
declare function getSession(
  request: HttpRequest, opts: { protected: true }
): Promise<asserts request is HttpRequest & {session: {account: {profile: Profile}}}>;

For now, it's not possible, necessitating a workaround.


One workaround involves reverting to pre-assertion function era tactics by having the function return its argument as the narrowed type, and then using the returned value thereafter instead of the initial argument. 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'd do the following:

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

Applied to your code example:

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

It may not be as seamless as using an assertion function, but it does the job!


Playground link to code

Answer №2

Essentially, the validation process occurs in the getSession function where your request is checked for validity. Once validated, you can assume that it contains specific properties. To streamline this process, consider creating two types: Session (with potentially undefined properties) and ValidSession (ensuring all necessary properties are present). Modify your HttpRequest interface to be generic:

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

This updated interface should be the return type of your validation function. In the end, you will have a function that takes an HttpRequest with possibly undefined properties but returns an HttpRequest with all required properties.

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Chaining asynchronous HTTP requests in Angular 2: A guide to stopping execution if one request fails

I am faced with a challenge of executing an array of HTTP requests in a specific order, where if any request fails, the subsequent ones should not be executed. Is there a way to achieve this requirement? What would be the recommended approach to hand ...

Unable to execute function on Child Element

I am encountering an issue when trying to call a function on a child component. Whenever I try to invoke it by clicking, I always receive an error indicating that the function is undefined. Here is the code in my Parent component: import {MatTableCompone ...

Angular - Navigate to Login Page post registration and display a confirmation message

As a newcomer to Angular, I am currently working on an Angular and Spring Boot application. So far, I have created components for user login and registration along with validation features. Now, my goal is to redirect the user to the login page upon succes ...

Upon running `npm run build` in vue.js, an error occurs stating that the interface 'NodeRequire' cannot extend types 'Require' simultaneously

ERROR in C:/phpStudy2018/PHPTutorial/WWW/Tms.Web/node_modules/@types/node/globals.d.ts(139,11): 139:11 The 'NodeRequire' interface cannot extend both 'Require' and 'RequireFunction' at the same time. The named property &apos ...

Implementing scrollIntoView() method in Typescript

Currently, I am focused on automating an AngularJS application. Our go-to automation language is TypeScript. My aim is to scroll to a specific element and then click on it. var element = element(by.className('learn-link')); browser.driver.exec ...

Can you use getters and setters in a TypeScript declaration file?

I am facing an issue with a declaration file for the openUi framework. The framework utilizes a get<propname>() and set<propname>(var) syntax for its properties. In traditional JavaScript, the setup would look like this: sap.ui.getCore().atta ...

Utilize SWR in NextJS to efficiently manage API redirection duplication

When using SWR to fetch data, I encountered an error where the default path of nextjs was repeated: http://localhost:3000/127.0.0.1:8000/api/posts/get-five-post-popular?skip=0&limit=5 Here is my tsx code: 'use client' import useSWR from &quo ...

Use PipeTransform to apply multiple filters simultaneously

Is it possible to apply multiple filters with PipeTransform? I attempted the following: posts; postss; transform(items: any[]): any[] { if (items && items.length) this.posts = items.filter(it => it.library = it.library ...

What is the proper way to reference the newly created type?

I came up with a solution to create a custom type that I can easily use without the need to constantly call useSession(), as it needs to be a client-side component. However, whenever I try to access this custom type, it always returns undefined (if I try t ...

The attribute 'finally' is not found on the data type 'Promise<void>'

I've been attempting to implement the finally method on a promise but continue running into this issue. Property 'finally' does not exist on type 'Promise<void>'. After researching similar problems, I found suggestions to a ...

What is the process for removing globally declared types in TypeScript definitions?

There are numerous libraries that cater to various platforms such as the web, Node servers, and mobile apps like React Native. This means they can be integrated using <script /> tags, require() calls, or the modern import keyword, particularly with t ...

What is the reason for the lack of compatibility between the TypeScript compilerOptions settings 'noEmitOnError: true' and 'isolatedModules: false'?

Whenever I try to execute TypeScript with isolatedModules set as true and then false, I keep encountering this error message: tsconfig.json(5,9): error TS5053: Option 'noEmitOnError' cannot be specified with option 'isolatedModules'. ...

Changing the font family for a single element in Next.js

One unique aspect of my project is its global font, however there is one element that randomly pulls font families from a hosted URL. For example: https://*****.com/file/fonts/Parnian.ttf My page operates as a client-side rendered application (CSR). So, ...

Tips for ensuring that eslint typescript identifies interfaces in .d.ts files

Currently, I am working on a project that involves Vite 3, Vuetify 3, and Vue 3 with the Volar extension. I have defined interfaces in some .d.ts files and also have ESLint installed. However, ESLint keeps complaining about not being able to find the inter ...

Using styled-components to enhance an existing component by adding a new prop for customization of styles

I am currently using styled-components to customize the styling of an existing component, specifically ToggleButton from material ui. However, I want my new component to include an additional property (hasMargin) that will control the style: import {Toggle ...

Is it possible to reactivate a stripe subscription that has been previously canceled

Currently, I am working on incorporating the functionality of resuming cancelled Stripe subscriptions. To achieve this, I am referring to the comprehensive guide available at: https://stripe.com/docs/billing/subscriptions/canceling-pausing Here is my appr ...

Tips for typing the following object/type in TypeScript

I need help with typing a user state in my application. I have the following: a user type which includes id, email, firstName, lastName, and verified fields. export type User = { id: string | null; email: string | null; firstName: string | null; l ...

Limiting the defaultValue of a select to one of the values of its options in TypeScript: A guide

Is there a way to configure the Select component's properties so that the defaultValue is limited to one of the predefined options values ("au" | "nz" in this scenario)? const countryOptions = [ { value: "au", l ...

Is it necessary for a TypeScript Library's repository to include the JavaScript version?

Is it necessary to include a JavaScript version of the library along with the Typescript repository for consumers? Or is it best to let consumers handle the compilation process themselves? Or should I consider another approach altogether? ...

Instance of "this" is undefined in Typescript class

After stumbling upon this code online, I decided to try implementing it in TypeScript. However, when running the code, I encountered an error: Uncaught TypeError: Cannot set property 'toggle' of null @Injectable() export class HomeUtils { p ...