Reputation: 354
I'm writing a React project using TypeScript, and I want to create a new component that takes a value that is an array of objects, and can create new objects to add to this array. When creating a new object, I want it to start with a blueprint that I define, that already includes some default values for some of the object's fields.
My problem is that I can't seem to get the blueprint to follow the value's generic type. The below (very simplified example) code is perfectly fine according to TypeScript:
type Props<T extends object> = {
value: Array<T>;
blueprint: T;
};
function MyComponent<T extends object>({}: Props<T>) {
return <div></div>;
}
function ParentComponent() {
return <MyComponent
value={[{ foo: "bar" }, { foo: "oof"}]}
blueprint={{ boo: "faz" }} // This doesn't throw a type-error, but I would like it to.
/>
}
I'd like TypeScript to throw a type-error when the blueprint
prop doesn't look like an object in the value
array. I've tried doing this by using a second generic type that extends or equals the first, but I've not managed to get to a solution.
Upvotes: 2
Views: 1035
Reputation: 328292
By default, the compiler will infer T
both from the value
property and the blueprint
property, often as a union of the types inferred from each property. But you want the compiler only to infer T
from value
and just verify that blueprint
matches that. In other words, you want the T
in blueprint
to be a non-inferential type parameter usage, as suggested in microsoft/TypeScript#14829.
This feature isn't quite officially supported, but there are a few ways to get this behavior. The first one is to "lower the inference priority" of the T
in the blueprint
property, by intersecting it with the nearly-top type {}
:
type NoInfer<T> = T & {};
type Props<T extends object> = {
value: Array<T>;
blueprint: NoInfer<T>;
};
Another way is to use conditional type deferral to prevent inference from happening:
type NoInfer<T> = [T][T extends any ? 0 : never];
type Props<T extends object> = {
value: Array<T>;
blueprint: NoInfer<T>;
};
In either version, blueprint
eventually is evaluated as being type T
(or nearly T
) but the inference is either delayed or prevented there. Both versions give you the following behavior for your example:
function ParentComponent() {
return <MyComponent
value={[{ foo: "bar" }, { foo: "oof" }]}
blueprint={{ boo: "faz" }} // error!
// Type '{ boo: string; }' is not assignable to type '{ foo: string; }'.
/>
}
which is what you wanted.
Not sure it's worth doing these workarounds as they may have some observable issues or edge cases in a wider code base. That's probably up to you to decide how badly your use case requires a no-infer. Okay, hope that helps; good luck!
Upvotes: 3
Reputation: 92274
This is because TypeScript has no way to know what the infer site is for T
. So it finds the lowest common denominator in both usages. In your case, Typescript considers your T
to be
{foo: string; boo?: undefined;} | {boo: string; foo?: undefined;}
The simplest solution is to specify in your call what type to use:
function ParentComponent() {
// does not compile
return <MyComponent<{foo: string}>
value={[{ foo: "bar" }, { foo: "oof"}]}
/>
}
If you really would like to not have to specify the params from the caller, you'd need some witchcraft to tell the compiler where T
is supposed to get its type from. You'd need a helper function that returns another function to generate your Props
. This way, Typescript knows that the parameter passed to the first function is the infer site.
function getProps<T extends object>(value: T[]): (blueprint: T) => Props<T> {
return function(blueprint: T) {
return {
value,
blueprint
};
}
}
const doesCompile = getProps([{a: 1}])({a:2});
const doesNotCompile = getProps([{a: 1}])({b: 2});
function ParentComponentWithFunctionCallCompiles() {
const props = getProps([{a: 2}])({a: 1});
return <MyComponent {...props} />
}
function ParentComponentWithFunctionCallDoesNotCompile() {
const props = getProps([{a: 2}])({b: 1});
return <MyComponent {...props} />
}
You can play with this on stackblitz
I've also asked a similar question before. See Enforce sub-type in array of generics. I decided to answer this question instead of just marking it a dupe because it does require a lot of grokking before understanding it. At least it did for me.
Upvotes: 1