Imagine a flexible component that takes a React.ComponentType
and its props
and renders it:
type Props<C> = {
component: React.ComponentType<C>;
componentProps: C;
otherProp: string;
};
const MyComponent = <C extends {}>(props: Props<C>) => {
return React.createElement(props.component, props.componentProps);
};
Can I somehow let MyComponent
receive the dynamic props
directly, e.g. like that (not working):
type Props<C> = {
component: React.ComponentType<C>;
otherProp: string;
};
const MyComponent = <C extends {}>(props: Props<C> & C) => {
const { otherProp, component, ...componentProps } = props;
return React.createElement(component, componentProps);
};
Error:
Error:(11, 41) TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is not assignable to parameter of type 'Attributes & C'.
Type 'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is not assignable to type 'C'.
'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is assignable to the constraint of type 'C', but 'C' could be instantiated with a different subtype of constraint '{}'.
Here we need to understand some utility types and how the destructuring happens in TS.
type Obj = {
[key: string]: any
}
interface I1 {
a: number
b: number
c: number
}
const i1: I1 = {
a: 1,
b: 1,
c: 1,
}
let {a, ...rest} = i1
interface Obj {
[key: string]: any
}
const i2: Obj & I1 = {
a: 1,
b: 1,
c: 1,
d: 1,
e: 1,
}
let {a: a1, b, c, ...rest2} = i2
function func<T extends Obj>(param: I1 & T) {
const {a, b, c, ...rest} = param
}
In the above code, the inferred type for rest
will be {b: number, c: number}
because object i1
contains only three keys and one of them aka a
is exhausted. In the case of rest2
, TS can still infer type to Obj
as keys from interface I1
are exhausted. By exhausted I mean they are not captured using rest operator.
But in case of function, TS is not able to do this type of inference. I don't know the reason why TS is not able to do. That may be due to limitation generics.
What happens in case of a function is that the type for rest
inside the function is Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">>
. Exclude
excludes keys a
, b
and c
from the generic type T
. Check Exclude here. Then, Pick
creates a new type from I1 & T
with keys returned by Exclude
. Since T
can be any type, TS is not able to determine the keys after exclusion and hence the picked keys and hence the newly created type even though T is constrained to Obj
. That's why the type variable rest
in the function remains Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">>
.
Please note that type returned by Pick
is a subtype of Obj
Now coming to the question, the same situation happens with componentProps
. The type inferred will be Pick<Props<C> & C, Exclude<keyof C, "otherProp" | "component">>
. TS will not be able to narrow it down. Looking at the signature of React.createElement
function createElement<P extends {}>(
type: ComponentType<P> | string,
props?: Attributes & P | null,
...children: ReactNode[]): ReactElement<P>
And calling it
React.createElement(component, componentProps)
The inferred type for P
in the signature will be C
in your code from the first argument i.e. component
because it has type React.ComponentType<C>
. The second argument should be either undefined
or null
or C
(ignoring Attributes
as of now). But the type of componentProps
is Pick<Props<C> & C, Exclude<keyof C, "otherProp" | "component">>
, which is definitely assignable to {}
but not to C
because it is subtype of {}
not of C
. C
is also a subtype of {}
but the pick type and C
may or may not be compatible (this is same as - there is a class A; B and C derives A, objects of B and C are assignable to A, but object of B is not ascribable to C). That's why the error
'Pick<Props<C> & C, Exclude<keyof C, "component" | "otherProp">>' is assignable to the constraint of type 'C', but 'C' could be instantiated with a different subtype of constraint '{}'.
As we are more intelligent than TS compiler, we know that they are compatible but TS does not. So make TS believe that we are doing correct, we can do a type assertion like this
type Props<C> = {
component: React.ComponentType<C>;
otherProp: string;
};
const MyComponent = <C extends {}>(props: Props<C> & C) => {
const { otherProp, component, ...componentProps } = props;
return React.createElement(component, componentProps as unknown as C);
// ------------------------------------------------^^^^^^^^
};
This is definitely a correct type assertion because we know that type of componentProps
will be C
Hope this answers your question and solves your problem.
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments