Search code examples
typescripttypescript-typingstypescript-genericsreact-functional-componentreact-typescript

How to partition a component's generic props and strongly type the results


i'm trying to partition a custom type back into it's individual elements:

type CustomType<T extends React.ElementType> = React.ComponentPropsWithoutRef<T> & { aBunchOfProps: string; }

code looks like following:

const partitionProps = <T extends React.ElementType>(
  props: CustomType<T>
): {
  customProps: { aBunchOfProps: string }, // named type
  componentProps: ComponentPropsWithoutRef<T>
} => {
  const {
    aBunchOfProps,
    ...componentProps
  } = props;

  const customProps = { aBunchOfProps };

  return { customProps, componentProps };
} // Error! componentProps: Omit<CustomType<T>, { aBunchOfProps }> is 
  // not assignable to type ComponentPropsWithoutRef<T>

which is weird because I'm able to assert type equality

type Equals<T, U> = T extends U ? (U extends T : true : false) : false;

type AreTheyEqual<T extends React.ElementType> = Equals<
  Omit<CustomType<T>, { aBunchOfProps: string }>,
  React.ComponentPropsWithoutRef<T>
>;
type UsingDiv = AreTheyEqual<'div'>; // true
type UsingA = AreTheyEqual<'a'>; // true
type UsingIFrame = AreTheyEqual<'iframe'>; //true

there should be maybe some conditional type to assert type equality in the partition function, but I can't quite figure it out

type AssertEquality<T extends React.ElementType> = Equals<T1, T2> extends true ? React.ComponentPropsWithoutRef<T> : never;

but that doesn't quite work either.

Any ideas?


Edit 11/1/21: See this typescript playground for a reproduction. We're able to force cast the return type as React.ComponentPropsWithoutRef<T> but it still leaves the function and exists inside of calling components as any.


Solution

  • The problem here is that you are using generic inside the function

    const partitionProps = <T extends React.ElementType>(
      props: CustomType<T>
    ): { /** ....some code */ }
    

    T is like black box, it will be known only during the call of a function, whereas comparison of :

    type UsingDiv = AreTheyEqual<'div'>; // true
    type UsingA = AreTheyEqual<'a'>; // true
    type UsingIFrame = AreTheyEqual<'iframe'>; //true
    

    is much easier to to because generic parameter of AreTheyEqual is known at compile time : div, a, iframe.

    Imagine, we don't have a generic type in our function:

    const partitionProps = (
        props: SomeHelpers & ComponentPropsWithoutRef<'a'>
      ): {
        customProps: { aBunchOfProps: string },
        componentProps: ComponentPropsWithoutRef<'a'>
      } => {
      const {
        aBunchOfProps,
        ...componentProps
      } = props;
      const customProps = { aBunchOfProps };
    
      return { customProps, componentProps };
    } 
    

    There are no errors, because TS is able to infer exact type of componentProps.

    Once you have provided T, TS is no more sure about type equality. Since, it is unsafe to return

    {
        customProps: { aBunchOfProps: string },
        componentProps: ComponentPropsWithoutRef<T>
      }
    

    However, you can loose the strict behavior. You can overload your function. Overloadings are bivariant.

    type SomeHelpers = { aBunchOfProps: string };
    
    type CustomType<T extends React.ElementType> = SomeHelpers &
      React.ComponentPropsWithoutRef<T>;
    
    function partitionProps<T extends React.ElementType<{ tag: number }>>(
      props: CustomType<T>
    ): {
      customProps: { aBunchOfProps: string }; // named type
      componentProps: ComponentPropsWithoutRef<T>;
    };
    function partitionProps<T extends React.ElementType>(props: CustomType<T>) {
      const { aBunchOfProps, ...componentProps } = props;
    
      const customProps = { aBunchOfProps };
    
      return { customProps, componentProps };
    }
    
    const result = partitionProps({
      aBunchOfProps: 'sdf',
      tag: 42
    });
    
    

    Please keep in mind that you also should provide a generic argument to React.ElementType because if you don't generic parameter will be any. See ElementType signature:

     type ElementType<P = any> =
            {
                [K in keyof JSX.IntrinsicElements]: P extends JSX.IntrinsicElements[K] ? K : never
            }[keyof JSX.IntrinsicElements] |
            ComponentType<P>;