Home > Software engineering >  Unable to assign props due to Typescripts nature of generics
Unable to assign props due to Typescripts nature of generics

Time:10-22

In this case I'm trying to make a HOC that would take control of a component that can't function without it.

In more detail: a HOC that takes a component that expects props value and onChange and returns a component that no longer expects those props, because they are taken care of.

This is what I got so far:

interface ControlledInput {
    value: string
    onChange: (newValue: string) => void
}

function withControl<T extends ControlledInput>(Elem: FC<T>){

    return (props: Omit<T, keyof ControlledInput>) => {

        const [value, setValue] = React.useState('')

        return <Elem value={value} onChange={setValue} {...props} /> // error
    }
}

However typescript gives me an error that props I'm passing to Elem are not assignable to type T. Link to codesandbox.

The error I get is discussed and explained here. TL;DR: Never assign concrete types to generic parameter, consider it read-only. Ironic that this conclusion is stated as solution. That's not a solution, I need to assign these props.

I've tried not using generic at all, but than prop types of a resulting component are lost.

Type casting helps but isn't it the same as putting any everywhere? If I just wanted to not see red squiggly lines I would use JavaScript

function withControl<T extends ControlledInput>(Elem: FC<T>) {
    return (props: Omit<T, keyof ControlledInput>) => {

        const [value, setValue] = React.useState('')

        return <Elem {...{ value, onChange: setValue, ...props } as T} /> // error is gone
    }
}

In ideal world I would like to tell TypeScript that I only care that component passed as Elem has props value, onChange and whatever else (the T). And resulting component expects as props what is left (the T). That's it, not very difficult on paper yet really difficult to describe to TS.

CodePudding user response:

The error:

  'T' could be instantiated with an arbitrary type which could be unrelated to '{ value: string; onChange: Dispatch<SetStateAction<string>>; }'.ts(2322)

is correct.

Consider this example:

interface ControlledInput {
  value: string;
  onChange: (newValue: string) => void;
}

interface FooProps {
  value: string;
  onChange: (newValue: string) => void;
  name: "hello";
}

const Foo: FC<FooProps> = () => null;

const withControl = <T extends ControlledInput>(Elem: FC<T>) => () =>
  // props: Omit<T, keyof ControlledInput>
  {
    const [value, setValue] = React.useState("");

    return <Elem value={value} onChange={setValue} />;
  };

withControl(Foo);

FooProps is a subtype of ControlledInput. This line withControl(Foo) compiles. But you ended up in a situation where Foo has required prop name whereas you have provided only value and onChange.

In order to fix it, just get rid of generic:

interface ControlledInput {
  value: string;
  onChange: (newValue: string) => void;
}

interface FooProps {
  value: string;
  onChange: (newValue: string) => void;
  name: "hello";
}

const Foo: FC<FooProps> = () => null;

const withControl = (Elem: FC<ControlledInput>) => () =>
  // props: Omit<T, keyof ControlledInput>
  {
    const [value, setValue] = React.useState("");

    return <Elem value={value} onChange={setValue} />;
  };

withControl(Foo); // expected error

UPDATE

could you please provide an example, where prop types of component passed as argument carry over to resulting component

You can use this pattern:

import React, { FC } from 'react'


interface MainProps {
  value: string;
  onChange: (value: string) => void
  name: string;
}

type ControlProps = {
  children: (value: string, setValue: (value: string) => void) => JSX.Element
}

const Control: FC<ControlProps> = ({ children }) => {
  const [value, setValue] = React.useState('');

  return children(value, setValue)
}

const Foo = (props: MainProps) => <div></div>;


const App =() => {
  <Control>{
    (value, onChange) =>
      <Foo value={value} onChange={onChange} name='hello' />
  }
  </Control>
}

Playground

CodePudding user response:

I did some experiments, apparently it's a TypeScript's limitation:

interface Foo {
    value: string
    name: string
    num: number
}

function test<T extends Foo>(data: T){

      let {value, ...rest} = data

      let t: T = {value, ...rest} // same error
}

It thinks that {value: string} & Omit<T, 'value'> and T are not the same. And honestly, I don't understand why.

Yet, when dealing with concrete types it works as expected

let foo: Foo = {name: '', value: '', num: 0}

let {num, ...rest} = foo

let foo2: Foo = {num, ...rest} // no error

TS Playground

Type casting it is than.

  • Related