简体   繁体   中英

React and Typescript: How to extend generic component props?

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, eg 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">> Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">> 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">> Pick<I1 & T, Exclude<keyof T, "a" | "b" | "c">> 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">> 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 ie 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">> 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM