本文详细介绍在 React 项目中使用 TypeScript 定义函数组件的四种主要方式,包括 React.FC、JSX.Element、直接接口定义及 PropsWithChildren。同时针对返回值限制、数组填充及泛型使用等常见开发陷阱提供解决方案与最佳实践。
前言
在基于 React 进行项目开发时,组件是最核心的构建模块。组件主要分为函数组件和类组件两种形式,我们可以参考如下定义方式:
函数组件的定义示例:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}类组件的定义示例:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}本文将重点讲解利用 TypeScript 定义函数式组件的四种具体方法,并梳理开发过程中需要关注的几个关键问题。
如何使用 TypeScript 定义函数式组件
标准的函数式组件通常接收一个 props 参数,并返回一个 JSX 元素或者 null。
在使用 TypeScript 定义函数组件时,存在四种可选方案。每种方案各有优劣,开发者应根据具体场景进行选择。
1. 使用 React.FC
由于 React 本身并非由 TypeScript 编写,其类型定义依赖于社区维护的 @type/react 包。该包提供了一个通用类型 FC,可用于为函数组件添加类型约束。
type FCProps = { text: string };
// React.FunctionComponent 的简写
const FCComponent: React.FC<FCProps> = ({ text = "" }) => <div>{text}</div>;上述代码中的 React.FC 实际上是 React.FunctionComponent 的简写形式。
当组件内部包含子元素时,TypeScript 编译器通常会发出警告:
type FCProps = { text: string };
const FCComponent: React.FC<FCProps> = ({ text = "" }) => <div>{text}</div>;
function App() {
return (
<div className="App">
<FCComponent text="Hello Chris1993.">
<span>children</span>
</FCComponent>
</div>
);
}具体的警告信息如下:
Type '{ children: string; text: string; }' is not assignable to type 'IntrinsicAttributes & FCProps'.
Property 'children' does not exist on type 'IntrinsicAttributes & FCProps'.目前官方已不推荐继续使用此方式,相关讨论可参考以下链接:
- Remove React.FC from Typescript template #8177
- 《TypeScript + React: Why I don't use React.FC》
2. 使用 JSX.Element
通过将 JSX.Element 类型指定为函数组件的返回值类型,若组件返回非 JSX.Element 类型的值,TypeScript 将报错提示。
type FCProps = { text: string };
const ElementComponent = ({ text }: FCProps): JSX.Element => <div>{text}</div>;
function App() {
return (
<div className="App">
<ElementComponent text="Hello Chris1993."></ElementComponent>
</div>
);
}3. 直接定义完整类型
由于 React 组件在包含子元素时,会隐式传递一个 children 属性,这可能导致参数类型定义出现偏差。因此,我们可以直接定义一个包含 children 属性类型的完整参数接口:
type FCProps = { text: string; children?: any };
const FCComponent: React.FC<FCProps> = ({ text = "" }) => <div>{text}</div>;
function App() {
return (
<div className="App">
<FCComponent text="Hello Chris1993.">
<span>children</span>
</FCComponent>
</div>
);
}4. 使用 React.PropsWithChildren
第三种方法要求每次手动编写 children 属性类型,较为繁琐。此时可以使用 React.PropsWithChildren 类型,该类型内部已封装了 children 的类型声明:
// react/index.d.ts
type PropsWithChildren<P> = P & { children?: ReactNode };因此,采用 React.PropsWithChildren 类型定义函数组件时,无需再单独处理 children 的类型问题:
type IProps = React.PropsWithChildren<{ text: string }>;
const PropsComponent = ({ text }: IProps) => <div>{text}</div>;
function App() {
return (
<div className="App">
<PropsComponent text="Hello Chris1993.">
<span>children</span>
</PropsComponent>
</div>
);
}使用过程需要注意的点
1. 函数式组件返回值不能是布尔值
在函数组件内部使用条件语句时,如果返回值是非 JSX 元素且非 null 的值,React 会抛出错误:
const ConditionComponent = ({ useRender = false }) =>
useRender ? <span>Render ConditionComponent</span> : false;// ❌
function App() {
return (
<div className="App">
<ConditionComponent useRender></ConditionComponent>
{/* 'ConditionComponent' cannot be used as a JSX component.
Its return type 'false | Element' is not a valid JSX element.
Type 'boolean' is not assignable to type 'ReactElement<any, any>'.
*/}
</div>
);
}正确的处理逻辑是确保函数组件返回有效的 JSX 元素或 null:
const ConditionComponent = ({ useRender = false }) =>
useRender ? <span>Render ConditionComponent</span> : <span>error</span>;// ✅
// or
const ConditionComponent = ({ useRender = false }) =>
useRender ? <span>Render ConditionComponent</span> : null;// ✅此外,当属性 useRender 的值为 true 时,若处理不当同样会引发错误:
const ConditionComponent = ({ useRender = false }) =>
useRender && <span>Render ConditionComponent</span>;// ❌2. 无法为组件使用 Array.fill() 填充
当组件直接返回 Array.fill() 的结果时,TypeScript 会提示类型错误。
const ArrayComponent = () => Array(3).fill(<span>Chris1993</span>); // ❌
function App() {
return (
<div className="App">
<ArrayComponent></ArrayComponent>
</div>
);
}具体的错误提示如下:
'ArrayComponent' cannot be used as a JSX component.
Its return type 'any[]' is not a valid JSX element.
Type 'any[]' is missing the following properties from type 'ReactElement<any, any>': type, props, key解决该问题的方法是明确指定函数的返回值类型:
const ArrayComponent = () =>
Array(3).fill(<span>Chris1993</span>) as any as JSX.Element; // ✅3. 支持使用泛型来创建组件
在基于 TypeScript 开发 React 函数式组件时,可以利用泛型进行约束,从而声明一个泛型组件(Generic Components),使组件具备更高的灵活性。
具体使用方式如下:
interface GenericProps<T> {
content: T;
}
const GenericComponent = <T extends unknown>(props: GenericProps<T>) => {
const { content } = props;
const component = <>{content}</>;
return <div>{component}</div>;
};
function App() {
return (
<div className="App">
{ /* Success ✅ */}
<GenericComponent<number> content={10} />
{ /* Error ❌ Type 'string' is not assignable to type 'number'. */}
<GenericComponent<number> content={"10"} />
</div>
);
}关于 Generic Components 章节中介绍的更高级用法:
interface Props<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
const List = <T extends unknown>(props: Props<T>) => {
const { items, renderItem } = props;
const [state, setState] = React.useState<T[]>([]); // You can use type T in List function scope.
return (
<div>
{items.map(renderItem)}
<button onClick={() => setState(items)}>Clone</button>
{JSON.stringify(state, null, 2)}
</div>
);
};
function App() {
return (
<div className="App">
<List<number>
items={[1, 2]} // type of 'string' inferred
renderItem={(item) => (
<li key={item}>
{/* Error: Property 'toPrecision' does not exist on type 'string'. */}
{item.toPrecision(3)}
</li>
)}
/>
</div>
);
}至此,关于使用 TypeScript 开发 React 函数组件的介绍已完成。如需了解更多 TypeScript 与 React 相关的内容,建议查阅本站往期文章或继续阅读后续系列文章,感谢大家的支持。