typescript - Feil returtype for betingede generiske typer


Jeg prøver å skrive en funksjon som returnerer en verdi ved å bruke generiske typer og betingede typer slik:

interface Foo<T> {
  bar: T extends true ? string : undefined;
  id: string;
}

interface Options<T> {
  withBar?: T;
}

function createFoo<T extends boolean>({ withBar }: Options<T>): Foo<T> {
  return {
    id: 'foo',
    ...(withBar && { bar: 'baz' }),
  };
}

Ovenstående gir følgende typefeil:

Type '{ bar?: "baz" | undefined; id: string; }' is not assignable to type 'Foo<T>'.
  Types of property 'bar' are incompatible.
    Type '"baz" | undefined' is not assignable to type 'T extends true ? string : undefined'.
      Type 'undefined' is not assignable to type 'T extends true ? string : undefined'.

Kan noen hjelpe meg å forstå hvorfor dette er tilfellet? Jeg spesifiserer at typen kan være udefinert, så det bør være tillatt å være udefinert.

I tillegg vil jeg gjerne få returtypen til funksjonen gitt visse parametere uten faktisk å kalle den. Er det mulig?

f.eks. ReturnType<typeof createFoo> vil ikke gi meg riktig type for bruk createFoo({ withBar: true }) fordi den ennå ikke vet om bruken.

Live eksempel her

Svar #1

Dette er et vanlig problem, de generiske typene løses ikke opp inne i funksjonsimplementasjonen. Du kan skrive en funksjons-overload for å få implementasjonen til å fungere:

interface Foo<T> {
  bar: T extends true ? string : undefined;
  id: string;
}

interface Options<T> {
  withBar?: T;
}

function createFoo<T extends boolean>({ withBar }: Options<T>): Foo<T>
function createFoo({ withBar }: Options<boolean>): Foo<true> | Foo<false> {
  // The implementation works because the return type is allowed to be Foo<true> or Foo<false>
  return {
    id: 'foo',
    ...(withBar && { bar: 'baz' }),
  };
}

// The first function overload is taken because it's the first one to match
// the call parameters
const foo = createFoo({ withBar: false }); // foo is of type Foo<false>

TypeScript-lekeplass

Svar #2

Det er en viktig forskjell mellom en påkrevd egenskap som kan ha en verdi eller undefined (nøkkelen er alltid til stede), og en valgfri egenskap (nøkkelen kan kanskje ikke finnes i objektet i det hele tatt). Se exactOptionalPropertyTypes

I innlegget ditt skrev du bar på Foo som alltid til stede, med en betinget verdi av string eller undefined, men du returnerte et objekt som kun hadde bar-nøkkelen betinget.

I modifikasjonen nedenfor skrev jeg på nytt Foo for å gjenspeile en valgfri egenskapstype for bar basert på den samme boolske betingelsen.

Jeg erstattet også spredningsoperatørsyntaksen med Object.assign (oppfylte kompilatoren), men jeg er ikke sikker på hvorfor dette gjør en forskjell.

Også verdt å merke seg: TS klager hvis du prøver å spre en ikke-objekt-type inn i et objekt (f.eks. undefined i innlegget ditt):

const obj = {...undefined};
/*           ^^^^^^^^^^^^
Error: Spread types may only be created from object types.(2698) */

TS-lekeplass

type StringId = { id: string };

type Foo<T> = T extends true ? StringId & { bar: string } : StringId;

interface Options<T> {
  withBar?: T;
}

function createFoo<T>({ withBar }: Options<T>): Foo<T> {
  return Object.assign({id: 'foo'}, (withBar && { bar: 'baz' }));
}

console.log(createFoo({withBar: true})); // { id: "foo", bar: "baz" }
console.log(createFoo({withBar: false})); // { id: "foo" }
console.log(createFoo({})); // { id: "foo" }

I tillegg vil jeg gjerne få returtypen til funksjonen gitt visse parametere uten faktisk å kalle den. Er det mulig?

Ja, ved å legge til en betinget returtype til funksjonen, som avhenger av parameteren:

TS-lekeplass

function createFoo<T extends boolean>({ withBar }: Options<T>): T extends true ? Foo<true> : Foo<false> {
  return Object.assign({id: 'foo'}, (withBar && { bar: 'baz' }));
}

const foo1 = createFoo({withBar: true}); // StringId & { bar: string }
const foo2 = createFoo({withBar: false}); // StringId

Relaterte artikler