I have a function that returns one of a handful of possible JSX elements. Each element has their own defined typescript props.
const Thing1 = (props) => <div>Thing 1</div>
const Thing2 = (props) => <div>Thing 2</div>
type Thing1Props = {
one: string;
two: string;
}
type Thing2Props = {
three: string;
four: string;
}
const elementList: { name: string; elm: (props: ElementList) => JSX.Element }[] = [
{ name: 'thing1', elm: (props: Thing1Props) => <Thing1 {...props} /> },
{ name: 'thing2', elm: (props: Thing2Props) => <Thing2 {...props} /> },
];
export type ElementList = Thing1Props | Thing2Props;
I'm getting an error from typescript that reads:
TS2322: Type '(props: { one: string; two: string;}) => JSX.Element' is not assignable to type '(props: ElementList) => Element'.
Types of parameters 'props' and 'props' are incompatible.
Type 'ElementList' is not assignable to type '{ one: string; two: string; }'.
Type '{ three: string; four: string; }' is missing the following properties from type '{ one: string; two: string; }': one, two
Typescript seems to be viewing ElementList as an AND instead of an OR. Not sure what to change here. I'm guessing because it doesn't know which element I'm calling upon it can't define the types.
Here's the issue in typescript playground
CodePudding user response:
The problem with the type { name: string; elm: (props: ElementList) => JSX.Element }
is elm
needs to be a function that accepts any possible argument of type ElementList
. Assuming you have the --strictFunctionTypes
compiler option enabled (and you should), then TypeScript will not allow a function of this type to choose to only accept some arguments of type ElementList
. Functions may not safely narrow their input types. But they can widen them; a function of type (props: ElementList | string) => JSX.Element
would be acceptable there, since if it accepts ElementList | string
then it definitely accepts ElementList
. It is said that functions are contravariant in their argument types; a specific instance of a function type can widen but not narrow its argument types. So this isn't the type you want.
Instead it seems that you want to represent the correlation between the name
property and the type of the props
parameter of elm
. So the actual type you want looks like:
type Elem = {
name: "thing1";
elm: (props: Thing1Props) => JSX.Element;
} | {
name: "thing2";
elm: (props: Thing2Props) => JSX.Element;
};
const elementList: Elem[] = [
{ name: 'thing1', elm: (props: Thing1Props) => <Thing1 {...props} /> },
{ name: 'thing2', elm: (props: Thing2Props) => <Thing2 {...props} /> },
]; // okay
That is, instead of having props
be a union type, you want the entire object to be a union type. In fact, this Elem
type is a discriminated union, where you can check the name
property to see which props
it needs:
function processElem(elem: Elem) {
switch (elem.name) {
case "thing1": return elem.elm({ one: "", two: "" });
case "thing2": return elem.elm({ three: "", four: "" });
}
}
You could write that Elem
type out by hand, but this could be tedious if you have a lot of Thing
s. Instead you could start with a mapping interface like
interface ThingPropMap {
thing1: Thing1Props,
thing2: Thing2Props
}
to represent the correlation, and then have the compiler evaluate Elem
like
type Elem = { [K in keyof ThingPropMap]:
{ name: K, elm: (props: ThingPropMap[K]) => JSX.Element }
}[keyof ThingPropMap]
, which is a distributive object type as coined in microsoft/TypeScript#47109. A distributive object type is of the form {[K in X]: F<K>}[X]
where X
is some keylike type or union of keylike types, and where F<K>
is some type function that operates on a keylike type. By immediately indexing into a mapped type, you end up getting the union of F<K>
for every K
in X
. So if X
is K1 | K2 | K3
, then the distributive object type is F<K1> | F<K2> | F<K3>
.
So the Elem
definition then computes the union of { name: K, elm: (props: ThingPropMap[K]) => JSX.Element }
for every K
in keyof ThingPropMap
.
Again, this is not necessary if you're willing to write out the discriminated union by hand.