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?

2 Answers 2

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.

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

Comments

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.

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.