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?
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.
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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With