Echo
Echo

Reputation: 631

Using conditional types in return type of promises

I'm calling an api from a server that returns an array of data from a mongo db. The schema of this data contains an ObjectID that references other models in the database. So there are two schemas, one where the property remains an ObjectID string and the other is a populated property where the string becomes an object. Below is the sample type definition, where prop1 can either be a string or an object.

type Base = {
    name: string;
    description?: string;
    prop1: string; // ObjectID
}

type Populated = {
    prop1: {
        name: string;
        description: string;
    }
}

With this, I made a conditional type that can determine whether the returned data is populated or not. The type is a generic that accepts either true or undefined. If the generic is true, it returns the Base type with the prop1 being the type defined in Populated, else it simply returns the Base type.

type Schema<T extends true | undefined> = T extends true ? Omit<Base, keyof Populated> & Populated : Base

Then, a function calls the api with the explicit return type (because api call defaults to any). It accepts an optional parameter that determines whether to populate the ObjectIDs or not.

const callApi = async <T extends true | undefined>(populate?: T): Promise<(Schema<typeof populate> & {_id: string})[]> =>  {
    // example only
    return {} as any
}

I tried to get the populated version of this schema, however the type remains string | { name: string; description: string; }

const test = async () => {
    const populated = await callApi(true)
    populated.map((data) => data.prop1)
    // data.prop1 is type `string | { name: string; description: string; }`,
    // should only be `{ name: string; description: string; }`
}

Here is a playground. Thanks for the help!

Upvotes: 1

Views: 523

Answers (1)

Tobias S.
Tobias S.

Reputation: 23825

There are two changes that need to be done:

type Schema<T extends true | undefined> = T extends true
  ? Omit<Base, keyof Populated> & Populated
  : Base;

const callApi = async <T extends true | undefined = undefined>(
  populate?: T
): Promise<(Schema<T> & { _id: string })[]> => {
  return {} as any;
};

You need to set a default type of undefined for the generic type T. When you call callApi without an argument, the type of T can not be inferred and inference falls back to the constraint of T. Since the constraint is a union of true | undefined, the conditional type distributes over the union and returns a union of both branches.

You also don't want to pass typeof populate to Schema since typeof populate evaluates to a union of T | undefined which is also distributed in the conditional type. Pass the generic type T instead.

const test = async () => {
  const populated1 = await callApi();
  populated1.map((data) => data.prop1);
  //                            ^? prop1: string

  const populated2 = await callApi(true);
  populated2.map((data) => data.prop1);
  //                            ^? prop1: { name: string; description: string; }
};

Playground

Upvotes: 1

Related Questions