TypeScript: innsnevring av returtype basert på boolsk alternativargument


Jeg prøver å lage en type for funksjonen min slik at returtypen endres basert på en verdi som sendes til den. Jeg har vurdert Typescript-returtype avhengig av parameter, og å følge det (nesten) bokstavelig ser ut til å fungere helt fint. I prosjektet mitt må jeg imidlertid bruke pilerfunksjoner. Jeg vil også gjerne unngå overbelastninger hvis mulig. Vurder denne signaturen og implementeringen:

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

når jeg kaller funksjonen slik:

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

Jeg får en typemismatch siden TS tror createDocumentList returnerer en Document[] og ikke den ønskede DocumentWithVersions[]. Jeg hadde inntrykk av at bruk av ternæroperatøren skulle gi TS nok informasjon til å forstå at returtypen ville endre seg basert på betingelsen.

For det fullstendige redigerbare eksemplet:

Svar #1

Hvis du gir opp på overbelastninger, men fortsatt ønsker en returtype som endres avhengig av parameteren, må du bruke en generisk funksjon som returnerer en betinget type. For eksempel:

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

Her er den generiske typeparameteren B begrenset til boolean og standardisert til false. Deretter, avhengig av om B er true eller false, er utdataene enten DocumentWithVersions[] eller Document[].


En forbehold med generiske funksjoner som returnerer betingede typer er at, inne i implementeringen av funksjonen, kan ikke kompilatoren verifisere at det du gjør er trygt. Der B er en uspesifisert generisk, utsetter kompilatoren evalueringen av B extends true ? ..., noe som betyr at den ikke kan se når en bestemt verdi matcher typen. Dette smertepunktet er temaet for den åpne saken 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[]'

Inntil og med mindre noe blir gjort for å ta tak i dette, betyr det at du må bruke typepåstander eller lignende for å unngå kompilatorfeil i implementeringen:

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

Nå kan vi teste at kall til createDocumentList() oppfører seg som du ønsker:

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>[]

Ser bra ut. Merk at documentList3 er en union av typer fordi withVersions-egenskapen til inputen er boolean, som er unionen true | false, og returtypen til createDocumentList() er distributiv over unioner. Dette kan være eller ikke være oppførselen du ønsker, men det ser rimelig ut for meg.

Lenke til kode på lekeplassen

Relaterte artikler