'Using Typescript generics for React Functional Components with eslint props validation
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?
Solution 1:[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.
Solution 2:[2]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | slhck |
Solution 2 |