javascript - Hvorfor krever ikke Typescript at funksjonen min returnerer en bestemt type?


Jeg har en generisk Factory-funksjon som skal returnere en spesifikk type:

type Factory<T> = () => T;

interface Widget {
  creationTime: number;
}

const build: Factory<Widget> = () => {
  return {
    creationTime: Date.now(),
    foo: 'bar',
  };
};

Jeg ville forventet at Typescript ville kaste en feil fordi foo ikke er en egenskap på grensesnittet Widget. Likevel gjør den det ikke.

Men hvis jeg endrer widgetFactory-funksjonen til koden nedenfor -- den eneste forskjellen er at jeg eksplisitt angir returtypen -- så kaster den en feil:

const build: Factory<Widget> = (): Widget => {
  return {
    creationTime: Date.now(),
    foo: 'bar',
  };
};

Finnes det en måte å få Typescript til å tildele samme «strenghet» til min generiske Factory-type?

Svar #1

Objekttyper i TypeScript forbyr generelt sett ikke ekstra egenskaper. De er "åpne" eller "utvidbare", i motsetning til "lukkede" eller "eksakte" (se microsoft/TypeScript#12936). Ellers ville det vært umulig å bruke underklasser eller grensesnittutvidelser:

interface FooWidget extends Widget {
   foo: string;
}
const f: FooWidget = { creationTime: 123, foo: "baz" };
const w: Widget = f; // okay

Noen ganger vil folk ha slike "eksakte" typer, men de er egentlig ikke en del av språket. I stedet har TypeScript overskytende egenskapskontroll, som bare skjer i veldig spesielle tilfeller: når et "ferskt" objektlitteral får en type som ikke kjenner til noen av egenskapene i objektlitteralet:

const x: Widget = { creationTime: 123, foo: "baz" }; // error, what's foo

Et objekt-literal er "ferskt" hvis det ikke har blitt tildelt noen type ennå. Den eneste forskjellen mellom x og w er at i x er literalen "fersk" og overflødige egenskaper er forbudt, mens i w er literalen... eh... "gammel" fordi den allerede har fått typen FooWidget.


Det kan virke som om din widgetFactory burde gi en feil, siden du returnerer objektlitteralen uten å tilordne den noe sted. Dessverre går ferskheten tapt i dette tilfellet. Det finnes et langvarig problem, microsoft/TypeScript#12632, som påpeker dette, og som avhenger av et veldig gammelt problem, microsoft/TypeScript#241. TypeScript utvider automatisk den returnerte typen når den sjekker om den er kompatibel med den forventede returtypen... og ferskheten går tapt. Det ser ut som om ingen liker dette, men det er vanskelig å fikse uten å ødelegge andre ting. Så foreløpig er det som det er.


Du har allerede en løsning: merk funksjonens returtype eksplisitt. Dette er ikke spesielt tilfredsstillende, men det gjør jobben.

export const WidgetFactory1: Factory<Widget> = {
   build: (): Widget => {
      return {
         creationTime: Date.now(),
         foo: 'bar', // error!
      };
   },
};

Andre løsninger som involverer å prøve å tvinge kompilatoren til å beregne eksakte typer er mulig, men betydelig styggere enn det du gjør:

const exactWidgetFactory =
   <W extends Widget & Record<Exclude<keyof W, keyof Widget>, never>>(
      w: Factory<W>) => w;

export const WidgetFactory2 = exactWidgetFactory({
   build: () => { // error!
// ~~~~~ <-- types of property foo are incompatible
      return {
         creationTime: Date.now(),
         foo: 'bar',
      };
   },
});

Så jeg vil foreslå å bare fortsette med det du har der.

Lenke til kode på lekeplassen

Relaterte artikler