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.