I'd like to take some code that is able to correctly determine when my types are incompatible, and use children
instead of a prop. Here's the baseline that errors correctly:
type Option<T> = {
value: T;
text: string;
}
type SelectProps<T> = {
value: T;
options: Option<T>[];
}
const options = [
{ value: 5, text: 'Five' },
{ value: 10, text: 'Ten'}
];
return <Select value="Text" options={options} />; // ERROR: value isn't type number
However I can't seem to get this to error when I use children:
type OptionProps<T> = {
value: T;
children: string;
}
type SelectProps<T> {
value: T;
children: React.ReactElement<OptionProps<T>>;
}
/* No errors here */
<Select value="Text">
<Option value={5}>Five</Option>
<Option value={10}>Ten</Option>
</Select>
I put together a more complete example in this codesandbox (the code from the sandbox can be found below): https://codesandbox.io/s/throbbing-sun-tg48b - Note how renderA
correctly identifies the error, where as renderB
incorrectly has no errors.
import * as React from 'react';
type OptionA<T> = {
value: T;
text: string;
}
type SelectAProps<T> = {
value: T;
options: OptionA<T>[];
onClick: (value: T) => void;
}
class SelectA<T> extends React.Component<SelectAProps<T>> {
renderOption = (option: OptionA<T>) => {
const { value, text } = option;
const onClick = () => {
this.props.onClick(value)
};
return <div onClick={onClick}>{text}</div>
}
render(): React.ReactNode {
return <div>{this.props.options.map(this.renderOption)}</div>
}
}
type OptionBProps<T> = {
value: T;
children: string;
}
class OptionB<T> extends React.Component<OptionBProps<T>> {}
type SelectBProps<T> = {
value: T;
children: React.ReactElement<OptionBProps<T>>[];
onClick: (value: T) => void;
}
class SelectB<T> extends React.Component<SelectBProps<T>> {
renderOption = (option: OptionB<T>) => {
const { value, children } = option.props;
const onClick = () => {
this.props.onClick(value)
};
return <div onClick={onClick}>{children}</div>
}
render(): React.ReactNode {
return <div>{React.Children.map(this.props.children, this.renderOption)}</div>
}
}
class Main extends React.Component {
onClick(value: string) {
console.log(value);
}
renderA(): React.ReactNode {
const options = [
{ value: 5, text: 'Five' },
{ value: 10, text: 'Ten'}
]
return <SelectA value="Text" options={options} onClick={this.onClick} />
}
renderB(): React.ReactNode {
return (
<SelectB value="Text" onClick={this.onClick}>
<OptionB value={5}>Five</OptionB>
<OptionB value={10}>Ten</OptionB>
</SelectB>
);
}
}
Up to now, unfortunately you cannot enforce type constraints on children
props like illustrated in your example. Before, I actually thought that this kind of type checking would be possible. Being curious, I researched a bit:
TypeScript will convert your JSX code to a vanilla React.createElement
statement via the default JSX factory function. The return type of React.createElement
is JSX.Element
(global JSX namespace), which extends React.ReactElement<any>
.
What that means is, when you render SelectB<T>
, your OptionB
components will be represented as JSX.Element[]
aka React.ReactElement<any>[]
.
Given your props type
type SelectBProps<T> = {
value: T;
children: React.ReactElement<OptionBProps<T>>[];
onClick: (value: T) => void;
}
, TypeScript is fine with comparing ReactElement<OptionBProps<T>>[]
to ReactElement<any>[]
and compiles successfully (given some concrete type for T
).
What TypeScript docs say
By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box.
You can prove this behavior (that JSX expressions resolve to JSX.Element
): Simply replace SelectBProps
by:
type SelectBProps<T> = { ... ; children: number[]; ... };
For each OptionB, you now get the error:
Type 'Element' is not assignable to type 'number'
Since TypeScript 2.8, locally scoped JSX namespaces are supported, which can help to use custom types/type checking for JSX.Element
.
TypeScript docs on locally scoped JSX namespaces
JSX type checking is driven by definitions in a JSX namespace, for instance JSX.Element for the type of a JSX element, and JSX.IntrinsicElements for built-in elements. Before TypeScript 2.8 the JSX namespace was expected to be in the global namespace, and thus only allowing one to be defined in a project. Starting with TypeScript 2.8 the JSX namespace will be looked under the jsxNamespace (e.g. React) allowing for multiple jsx factories in one compilation.
In addition, one quote from this very helpful article:
TypeScript supports locally scoped JSX to be able to support various JSX factory types and proper JSX type checking per factory. While current react types use still global JSX namespace, itβs gonna change in the future.
-
Finally, it may be also reasonable to declare children
types
JSX.Element
from an array of elements.If you still want to have extensive type checking, maybe sticking to your previous solution with props may be a valid idea? At last, children
are just another form of props
and Render Props have established as a solid concept.
While that does not solve your original children
type checking case, I hope to have cleared things up a bit (as for me, it did π)!
Cheers
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With