Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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?

like image 847
RobertAKARobin Avatar asked May 15 '20 02:05

RobertAKARobin


People also ask

Should you specify return type in TypeScript?

Functions in TypeScript often don't need to be given an explicit return type annotation. Leaving off the return type is less code to read or write and allows the compiler to infer it from the contents of the function. However, explicit return types do make it visually more clear what type is returned by a function.

Is type mandatory in TypeScript?

Forcing optional types to be required in TypeScript As with other utility types, Required is meant to work with an interface or object type, like the User type we defined above. As such, it doesn't work with variables. This doesn't matter much, though, since a variable cannot have an empty value anyway.

How do you avoid using a type in TypeScript?

any. ❌ Don't use any as a type unless you are in the process of migrating a JavaScript project to TypeScript. The compiler effectively treats any as “please turn off type checking for this thing”. It is similar to putting an @ts-ignore comment around every usage of the variable.


1 Answers

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

like image 159
jcalz Avatar answered Oct 02 '22 15:10

jcalz