5

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 '{}'.

1 Answer 1

8

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.