John Down
John Down

Reputation: 77

Typescript generic type for a React component

I'm trying to figure out how to write the correct type for the Data entry so that it accepts any component with props

Example:

const A = (props: { a: number }) => <>{props.a}</>
const B = (props: { b: string }) => <>{props.b}</>

type Data = {
  [id: string]: <P>(props: P) => JSX.Element | null
}

const data: Data = {
  aa: (props: Parameters<typeof A>[0]) => A(props),
  bb: (props: Parameters<typeof B>[0]) => B(props)
}

const Usage = () => (
  <>
    A: {data.aa({ a: 12 })}
    B: {data.bb({ b: 'hi' })}
  </>
)

Even though I've specified prop types for each component, I'm still getting this error:

TS2322: Type '(props: Parameters<typeof A>[0]) => JSX.Element' is not assignable to type '<P>(props: P) => Element | null'.
  Types of parameters 'props' and 'props' are incompatible.
    Type 'P' is not assignable to type '{ a: number; }'

What should I write in the Data type so it accepts any component?

Upvotes: 1

Views: 2276

Answers (2)

mheskamp
mheskamp

Reputation: 94

I like what Chris Hamilton mentioned with using functional components class from react. I read this question more about how to deal with typescript generic types with constraints. The best I could see is to create a union type of AProps and BProps for defining the type constraint in the Data type definition and using typeguards to enforce what is used in each Data function. It however, would get unwieldy if you want this to work for any type of component at all because you have to define the union type for all possible components.

type AProps = { a: number };
type BProps = { b: string };

const A = (props: AProps) => <>{props.a}</>
const B = (props: BProps) => <>{props.b}</>

type GenericParams = AProps | BProps;

type Data = {
    [id: string]: <P extends GenericParams>(props: P) => JSX.Element | null
}

const data: Data = {
    aa: (props: GenericParams) => isAProps(props) ? A(props) : null,
    bb: (props: GenericParams) => isBProps(props) ? B(props) : null
}

const Usage = () => (
    <>
        A: {data.aa({ a: 12 })}
        B: {data.bb({ b: 'hi' })}
    </>
)

function isAProps(props: GenericParams): props is AProps
{
    if ("a" in props && typeof props.a === "number")
        return true;

    return false;
}

function isBProps(props: GenericParams): props is BProps
{
    if ("b" in props && typeof props.b === "string")
        return true;

    return false;
}

Upvotes: 0

Chris Hamilton
Chris Hamilton

Reputation: 10994

Provided you have installed @types/react and @types/react-dom you will have most react types already defined.

For Function Components there is the type FunctionComponent or FC for short. It is also generic so you can specify the props type.

import type { FC } from 'react';

const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;

type Data = {
  [id: string]: FC<any>;
};

const data: Data = {
  aa: A,
  bb: B,
};

const Usage = () => (
  <>
    A: {data.aa({ a: 12 })}
    B: {data.bb({ b: 'hi' })}
  </>
);

demo: https://stackblitz.com/edit/react-ts-y9ejme?file=App.tsx


Note however you lose the props type by typing an object as Data, because you're allowing any props types. Typescript will not see that b is supposed to be a number, not a string. It would be better to exclude that type and just let typescript infer the type from the object literal.

const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;

const data = {
  aa: A,
  bb: B,
};

export default function App() {
  return (
    <>
      A: {data.aa({ a: 12 })}
      {/* Error: Type 'string' is not assignable to type 'number'. */}
      B: {data.bb({ b: 'hi' })} 
    </>
  );
}

demo: https://stackblitz.com/edit/react-ts-j8uagi?file=App.tsx


Otherwise, you'll have to explicitly type the properties:

const A: FC<{ a: number }> = ({ a }) => <>{a}</>;
const B: FC<{ b: number }> = ({ b }) => <>{b}</>;

type Data = {
  aa: typeof A;
  bb: typeof B;
};

const data: Data = {
  aa: A,
  bb: B,
};

export default function App() {
  return (
    <>
      A: {data.aa({ a: 12 })}
      {/* Error: Type 'string' is not assignable to type 'number'. */}
      B: {data.bb({ b: 'hi' })}
    </>
  );
}

demo: https://stackblitz.com/edit/react-ts-ykbq8q?file=App.tsx


Or ensure that the properties you are accessing are common for all components:

const A: FC<{ a: number }> = ({ a }) => <>I'm an A {a}</>;
const B: FC<{ a: number, b: number }> = ({ a, b }) => <>I'm a B {a}</>;

type Data = {
  [id: string]: FC<{ a: number }>;
};

const data: Data = {
  aa: A,
  bb: B,
};

export default function App() {
  return (
    <>
      A: {data.aa({ a: 12 })}
      {/* Error: Type 'string' is not assignable to type 'number'. */}
      B: {data.bb({ a: 'hi' })}
      {/* You can pass b here, but it won't be typechecked */}
    </>
  );
}

demo: https://stackblitz.com/edit/react-ts-atcwwh?file=App.tsx


There's other options, but it really depends on your use case.

Upvotes: 1

Related Questions