Search code examples
typescripttypesinterface

Typescript type extension


I try to define a custom interfaces like this :

export interface IAPIRequest<B extends any, P extends any, Q extends any>
{
  body: B;
  params: P;
  query: Q;
}

This type is supposed to be extended in a lot of other types for each request mu API is supposed to handle.

For example :

export interface ILoginRequest extends IAPIRequest<{ email: string; password: string; }>, undefined, undefined> {}

It works a little but everytime I use this interface, I must provide all the properties even if they are undefined.

Example:

const login = async ({ body }: ILoginRequest) => 
{
  ...
}

const response = await login({ body: { email: 'mail@test.com', password: 'verystrongpassword' }, params: undefined, query: undefined });

It doesn't work if I don't provide the undefined properties.

How can I define an abstract type for IAPIRequest that would avoid me from providing undefined values ?

PS : I've tried this as well

export interface IAPIRequest<B extends any, P extends any, Q extends any>
{
  body?: B;
  params?: P;
  query?: Q;
}

Even for IAPIRequest<B, P, Q> where none of B, P, or Q allow undefined, I still get that the properties might be undefined


Solution

  • TypeScript doesn't automatically treat properties that accept undefined to be optional (although the converse, treating optional properties as accepting undefined, is true, unless you've enabled --exactOptionalPropertyTypes). There is a longstanding open feature request for this at microsoft/TypeScript#12400 (the title is about optional function parameters, not object properties, but the issue seems to have expanded to include object properties also). Nothing has been implemented there, although the discussion describes various workarounds.

    Let's define our own workaround; a utility type UndefinedIsOptional<T> that produces a version of T such that any property accepting undefined is optional. It could look like this:

    type UndefinedIsOptional<T extends object> = (Partial<T> &
        { [K in keyof T as undefined extends T[K] ? never : K]: T[K] }
    ) extends infer U ? { [K in keyof U]: U[K] } : never
    

    That's a combination of Partial<T> which turns all properties optional, and a key remapped type that suppresses all undefined-accepting properties. The intersection of those is essentially what you want (an intersection of an optional prop and a required prop is a required prop) but I use a technique described at How can I see the full expanded contract of a Typescript type? to display the type in a more palatable manner.

    Then we can define your type as

    type IAPIRequest<B, P, Q> = UndefinedIsOptional<{
        body: B;
        params: P;
        query: Q;
    }>
    

    and note that this must be a type alias and not an interface because the compiler needs to know exactly which properties will appear (and apparently their optional-ness) to be an interface. This won't matter much with your example code but you should be aware of it.

    Let's test it out:

    type ILR = IAPIRequest<{ email: string; password: string; }, undefined, undefined>
    /* type ILR = {
        body: {
            email: string;
            password: string;
        };
        params?: undefined;
        query?: undefined;
    } */
    

    That looks like what you wanted, so you can define your ILoginRequest interface:

    interface ILoginRequest extends IAPIRequest<
        { email: string; password: string; }, undefined, undefined> {
    }
    

    Also, let's just look at what happens when the property includes undefined but is not only undefined:

    type Other = IAPIRequest<{ a: string } | undefined, number | undefined, { b: number }>;
    /* type Other = {
        body?: {
            a: string;
        } | undefined;
        params?: number | undefined;
        query: {
            b: number;
        };
    } */
    

    Here body and params are optional because undefined is possible, but query is not because undefined is impossible.

    Playground link to code