简体   繁体   中英

Why isn't Typescript requiring my function to return a certain type?

I have a generic Factory function that should return a specific type:

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

interface Widget {
  creationTime: number;
}

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

I would expect Typescript to throw an error because foo is not a property on the interface Widget. However, it does not.

But if I modify the widgetFactory function to the below code -- the only difference being that I explicitly declare the return type -- then it does throw an error:

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

Is there a way to make Typescript assign the same "strictness" to my generic Factory type?

Object types in TypeScript do not in general prohibit extra properties. They are "open" or "extendible", as opposed to "closed" or "exact" (see microsoft/TypeScript#12936 ). Otherwise it would be impossible to use subclasses or interface extensions:

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

Sometimes people want such "exact" types, but they're not really part of the language. Instead, what TypeScript has is excess property checking , which only happens in very particular circumstances: when a "fresh" object literal is given a type that doesn't know about some of the properties in the object literal:

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

An object literal is "fresh" if it hasn't been assigned to any type yet. The only difference between x and w is that in x the literal is "fresh" and excess properties are forbidden, while in w the literal is... uh... "stale" because it has already been given the type FooWidget .


From that it might seem that your widgetFactory should give an error, since you are returning the object literal without assigning it anywhere. Unfortunately, freshness is lost in this case. There's a longstanding issue, microsoft/TypeScript#12632 , that notes this, and depends on a very old issue, microsoft/TypeScript#241 . TypeScript automatically widens the returned type when checking to see if it's compatible with the expected return type... and freshness is lost. It looks like nobody likes this, but it's hard to fix it without breaking other things . So for now, it is what it is.


You already have one workaround: explicitly annotate the function's return type. This isn't particularly satisfying, but it gets the job done.

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

Other workarounds involving trying to force the compiler to compute exact types are possible but significantly uglier than what you're doing:

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

So I'd suggest just continuing with what you've got there.


Okay, hope that helps; good luck!

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM