1

I want to create a functional component in React using Typescript, and I have already set it up according to this Q&A:

export interface CustomProps<T extends object> extends SomeOtherProps<T> {
  customProp?: number;
}

const CustomComponent: <T extends object>(props: CustomProps<T>) => JSX.Element = {
  customProp = 10,
  ...props
}) => {
  // etc
};

However, this gives me an error in eslint saying that the props are not validated:

'customProp' is missing in props validation

I can try to add props validation with the generic by adding CustomProps after the default props:

export interface CustomProps<T extends object> extends SomeOtherProps<T> {
  customProp?: number;
}

const CustomComponent: <T extends object>(props: CustomProps<T>) => JSX.Element = {
  customProp = 10,
  ...props
}: CustomProps<any>) => {
  // etc
};

But this gives me a warning with "no explicit any". And if I insert T, it won't know about it. So how do I address this?

slhck
  • 36,575
  • 28
  • 148
  • 201

2 Answers2

1

The solution lies in instantiating the type again, just like it was instantiated for the component declaration itself:

export interface CustomProps<T extends object> extends SomeOtherProps<T> {
  customProp?: number;
}

const CustomComponent: <T extends object>(props: CustomProps<T>) => JSX.Element = <T extends object>({
  customProp = 10,
  ...props
}: CustomProps<T>) => {
  // etc
});

This way, all props will be properly validated.

slhck
  • 36,575
  • 28
  • 148
  • 201
1

Answer Overview

I thought I'd give a simplified answer that demonstrates the full use of TypeScript Generics in Functional React Components, since it took me a full day to figure out how to do this properly. The file extension is assumed to be ".tsx".

I demonstrate the Class-Based React Component equivalents first, so you can compare them.

This also demonstrates the access of sub-properties on types.

Define Some Types That Will Be Used and Their TypeScript "typeof" equivalent checks:

const isString = (x: any): x is string => typeof x === 'string';

interface A {
  a: string;
}
const isA = (x: any): x is A => !!x.a;

interface B {
  b: string;
}
const isB = (x: any): x is B => !!x.b;

interface Props<T> {
  thing: T;
}

Simple Class-Based Component Example

class ClassCompY<T extends A> extends React.Component<Props<T>> {
  render() {
    const { thing } = this.props;
    return <div>{thing.a}</div>;
  }
}

This is just here for comparison purposes.

Complex Class-Based Component Example

class ClassCompZ<T extends string | A | B> extends React.Component<Props<T>> {
  render() {
    const { thing } = this.props;

    if (isString(thing)) {
      return <div>{thing}</div>;
    }

    if (isB(thing)) {
      return <div>{(thing as B).b}</div>;
    }

    return <div>{(thing as A).a}</div>;
  }
}

This is just here for comparison purposes.

Simple Functional Component Example

const FuncCompA = <T extends A>(props: Props<T>) => {
  const { thing } = props;

  return <div>{thing.a}</div>;
};

Complex Functional Component Example

const FuncCompB = <T extends string | A | B>(props: Props<T>) => {
  const { thing } = props;

  if (isString(thing)) {
    return <div>{thing}</div>;
  }

  if (isA(thing)) {
    return <div>{(thing as A).a}</div>;
  }

  return <div>{(thing as B).b}</div>;
};

Functional Component that takes React Children

const FuncCompC = <T extends A>(props: Props<T> & { children?: React.ReactNode }) => {
  const { thing, children } = props;
  return (
    <>
      <div>{thing.a}</div>
      <div>{children}</div>
    </>
  );
};

You can append the props: Props<T> in any of the other components with & { children?: React.ReactNode } to allow them to use children as well.

Calling all of the above components in JSX

const WrapperComponent = () => {
  return (
    <div>
      <ClassCompY thing={{ a: 'ClassCompY, Type: A' }} />
      <ClassCompZ thing='ClassCompZ, Type: string' />
      <ClassCompZ thing={{ a: 'ClassCompZ, Type: A' }} />
      <ClassCompZ thing={{ b: 'ClassCompZ, Type: B' }} />
      <FuncCompA thing={{ a: 'FuncCompA, Type: A' }} />
      <FuncCompB thing='FuncCompB, Type: string' />
      <FuncCompB thing={{ a: 'FuncCompB, Type: A' }} />
      <FuncCompB thing={{ b: 'FuncCompB, Type: B' }} />
      <FuncCompC thing={{ a: 'FuncCompC, Type: A' }}>ChildOfFuncCompC</FuncCompC>
    </div>
  );
};

Simply put this component in your app to see it working and to mess around with the components themselves to see what causes them to break. I only included the code that is strictly necessary not to break them.

Leaving out "extends SomeType"

TypeScript will complain about a sub-property like "a" or "b" not being found on the type "T".

Leaving out "(something as SomeType)"

TypeScript will complain about the types not matching up.