Kosmotaur
Kosmotaur

Reputation: 1786

TypeScript: return type narrowing based on boolean option argument

I am trying to create a type for my function so that the return type changes based on a value passed to it. I've considered Typescript return type depending on parameter and following that to (almost) the letter seems to work just fine. In my project however, I must use arrow functions. Also, I'd like to avoid overloads if at all possible. Consider this signature and implementation:

type CreateDocumentListOptions = {
  withVersions?: boolean;
};

type CreateDocumentList = (
  options?: CreateDocumentListOptions
) => CreateDocumentListOptions["withVersions"] extends true
  ? DocumentWithVersions[]
  : Document[];

const createDocumentList: CreateDocumentList = ({
  withVersions = false
} = {}) =>
  times(
    withVersions ? createDocumentWithVersions : createDocument,
    chance.natural({ min: 1, max: 10 })
  );

when I call the function like this:

const documentList: DocumentWithVersions[] = createDocumentList({
  withVersions: true
});

I get a type mismatch as TS thinkgs createDocumentList returns a Document[] not the desired DocumentWithVersions[]. I was under the impression that using the ternary operator would be enough information for TS to understand that the return type would change based on its condition.

For the complete editable example: Edit pedantic-newton-xx4sp

Upvotes: 3

Views: 1882

Answers (1)

jcalz
jcalz

Reputation: 330571

If you are giving up on overloads, but you still want a return type that switches depending on the parameter, then you will need to use a generic function returning a conditional type. For example:

type CreateDocumentList = <B extends boolean = false>(
    options?: { withVersions?: B }
) => B extends true
    ? DocumentWithVersions[]
    : Document[];

Here the generic type parameter B is constrained to boolean and defaults to false. Then, depending on whether B is true or false, the output is either DocumentWithVersions[] or Document[].


One caveat with generic functions returning conditional types is that, inside the implementation of the function, the compiler cannot verify that what you are doing is safe. Where B is an unspecified generic, the compiler defers evaluation of B extends true ? ..., meaning it can't see when any particular value matches the type. This pain point is the subject of the open issue microsoft/TypeScript#33912.

const createDocumentListOops: CreateDocumentList = (({
    withVersions = false
} = {}) =>
    times(
        withVersions ? createDocumentWithVersions : createDocument,
        chance.natural({ min: 1, max: 10 })
    )); // error!
// Type 'Document[]' is not assignable to type 
// 'B extends true ? Required<Document>[] : Document[]'

Until and unless anything is done to address this, it means you will need to use type assertions or the like to avoid compiler errors in the implementation:

const createDocumentList = (({
    withVersions = false
} = {}) =>
    times(
        withVersions ? createDocumentWithVersions : createDocument,
        chance.natural({ min: 1, max: 10 })
    )) as CreateDocumentList; // okay

Now we can test that calls to createDocumentList() behave as you desire:

const documentList = createDocumentList({ withVersions: true });
// const documentWithVersionsList: Required<Document>[]

const documentList1 = createDocumentList();
// const documentList1: Document[]

const documentList2 = createDocumentList({ withVersions: false });
// const documentList2: Document[]

const documentList3 = createDocumentList({ withVersions: Math.random() < 0.5 });
// const documentList3: Document[] | Required<Document>[]

Looks good. Note that documentList3 is a union of types because the withVersions property of the input is boolean, which is the union true | false, and the return type of createDocumentList() is distributive over unions. This may or may not be the behavior you want, but it looks reasonable to me.

Playground link to code

Upvotes: 3

Related Questions