React中的TypeScript
约 2404 字大约 8 分钟
TypeScriptReact
2025-08-17
props类型
在 TypeScript 中,组件的 props 类型既可以直接内联定义,也可以通过 type
或 interface
来抽离定义。
内联类型
这是最简单的写法,直接在函数参数处为 props 提供类型:
function MyButton({ title, disabled }: { title: string; disabled: boolean }) {
return <button disabled={disabled}>{title}</button>;
}
优点:简洁、直观,适合 props 很少的场景。 缺点:一旦 props 字段多起来,就会显得冗长、难以维护。
使用 interface
定义 Props
interface MyButtonProps {
/** 按钮文字 */
title: string;
/** 按钮是否禁用 */
disabled: boolean;
}
function MyButton({ title, disabled }: MyButtonProps) {
return <button disabled={disabled}>{title}</button>;
}
更适合多人协作和可维护性较强的项目。
使用 type
定义 Props
type MyButtonProps = {
title: string;
disabled: boolean;
};
function MyButton({ title, disabled }: MyButtonProps) {
return <button disabled={disabled}>{title}</button>;
}
type
和 interface
在大部分场景下功能相似,区别是:
interface
支持 扩展(extends),适合用于继承/合并。type
支持 联合类型、交叉类型 等高级组合。
使用联合类型(Union Types)
当某个 prop 允许多种类型时,可以用联合类型:
type Status = "loading" | "success" | "error";
interface StatusBadgeProps {
status: Status;
}
function StatusBadge({ status }: StatusBadgeProps) {
return <span>Status: {status}</span>;
}
/*
<StatusBadge status="loading" />
<StatusBadge status="success" />
*/
这样 TS 会自动校验,传入错误的值会报错。
从已有类型派生新类型
TypeScript 提供了 工具类型 来从已有类型中创建新类型。常见的有:
Partial<T>
:将所有字段变为可选Pick<T, K>
:只取部分字段Omit<T, K>
:排除部分字段
interface User {
id: number;
name: string;
age: number;
email: string;
}
// 只取 id 和 name
type UserPreview = Pick<User, "id" | "name">;
// 去掉 email
type UserWithoutEmail = Omit<User, "email">;
// 全部字段可选
type UserOptional = Partial<User>;
/*
function UserCard({ id, name }: UserPreview) {
return <div>{id} - {name}</div>;
}
*/
更复杂的 Props 示例
支持默认值、可选字段、事件回调:
interface InputProps {
label: string;
value: string;
placeholder?: string; // 可选字段
onChange: (value: string) => void; // 回调
}
function TextInput({ label, value, placeholder, onChange }: InputProps) {
return (
<label>
{label}
<input
value={value}
placeholder={placeholder ?? "请输入内容"}
onChange={(e) => onChange(e.target.value)}
/>
</label>
);
}
Hooks类型
React 的类型定义(@types/react
)已经为内置的 Hooks 提供了很好的类型支持,所以大部分情况下你根本不需要额外写类型。
但有时候我们希望 显式标注类型,让代码更清晰、更健壮。
useState
作用:在函数组件中添加“可记忆的”状态。
类型推断:useState
会根据你传入的初始值来推断类型:
const [enabled, setEnabled] = useState(false);
// 推断: enabled => boolean, setEnabled => (value: boolean | (prev: boolean) => boolean)
但有时候需要显式指定:
const [enabled, setEnabled] = useState<boolean>(false);
联合类型
更常见的情况是联合类型,例如请求状态:
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
复杂对象状态
如果多个状态相关联,推荐使用对象并结合联合类型:
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: any }
| { status: 'error'; error: Error };
const [requestState, setRequestState] = useState<RequestState>({ status: 'idle' });
useReducer
作用:适合管理复杂状态逻辑,类似 Redux 的 reducer 模式。
import { useReducer } from 'react';
interface State {
count: number;
}
type CounterAction =
| { type: "reset" }
| { type: "setCount"; value: number };
const initialState: State = { count: 0 };
function stateReducer(state: State, action: CounterAction): State {
switch (action.type) {
case "reset": return initialState;
case "setCount": return { ...state, count: action.value };
default: throw new Error("Unknown action");
}
}
export default function App() {
const [state, dispatch] = useReducer(stateReducer, initialState);
return (
<div>
<h1>计数器</h1>
<p>值:{state.count}</p>
<button onClick={() => dispatch({ type: "setCount", value: state.count + 5 })}>+5</button>
<button onClick={() => dispatch({ type: "reset" })}>重置</button>
</div>
);
}
类型关键点:
State
接口描述了状态结构。CounterAction
联合类型描述所有可能的 action。stateReducer
显式设置参数和返回值类型。useReducer
自动推断state
和dispatch
的类型。
useContext
作用:避免 props 层层传递,提供一个全局或局部的共享数据通道。
简单场景:当有合理的默认值时:
import { createContext, useContext, useState } from 'react';
type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme>("system");
const useTheme = () => useContext(ThemeContext);
export default function MyApp() {
const [theme] = useState<Theme>("light");
return (
<ThemeContext.Provider value={theme}>
<MyComponent />
</ThemeContext.Provider>
)
}
function MyComponent() {
const theme = useTheme();
return <p>当前主题:{theme}</p>;
}
无默认值场景:有时 null 是默认值,此时要在 Hook 中做检查:
import { createContext, useContext, useMemo } from 'react';
type ComplexObject = { kind: string };
const Context = createContext<ComplexObject | null>(null);
const useComplex = () => {
const value = useContext(Context);
if (!value) throw new Error("useComplex 必须在 Provider 内使用");
return value;
};
export default function MyApp() {
const object = useMemo(() => ({ kind: "complex" }), []);
return (
<Context.Provider value={object}>
<MyComponent />
</Context.Provider>
);
}
function MyComponent() {
const obj = useComplex();
return <p>当前对象:{obj.kind}</p>;
}
useMemo
作用:只有在依赖项改变时才重新计算值,避免重复计算。
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
- 类型会根据
filterTodos
的返回值自动推断。 - 如果想更明确,可以写
<ReturnType<typeof filterTodos>>
。
注意:React Compiler 将来会自动帮你做记忆化,很多时候可能不再需要手写 useMemo
。
useCallback
作用:当依赖不变时,返回相同的函数引用,常用于优化子组件渲染。
const handleClick = useCallback(() => {
console.log("clicked!");
}, [todos]);
在严格模式下,推荐显式为事件处理函数标注类型:
const handleChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => setValue(e.currentTarget.value),
[]
);
DOM类型
在 React 中,所有 DOM 事件都会被封装成 合成事件(SyntheticEvent)。 这意味着你并不是直接操作原生 DOM 事件,而是 React 提供的跨浏览器兼容层。
通用规则
- 事件推断:大多数时候,直接写内联处理函数即可,TypeScript 会自动推断事件类型。
- 提取函数:如果把事件处理函数单独声明,就需要手动标注事件类型。
- 基类:所有事件都继承自
React.SyntheticEvent<T>
,其中T
是触发事件的元素类型。
表单相关
// 输入框变更
function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
// 表单提交
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
console.log("表单提交");
}
鼠标事件
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log("点击按钮");
}
function handleDivEnter(e: React.MouseEvent<HTMLDivElement>) {
console.log("鼠标进入 div");
}
键盘事件
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
console.log("回车键");
}
}
焦点事件
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
console.log("获得焦点");
}
function handleBlur(e: React.FocusEvent<HTMLInputElement>) {
console.log("失去焦点");
}
拖拽事件
function handleDrag(e: React.DragEvent<HTMLDivElement>) {
console.log("拖拽中...");
}
其他常用事件
// 输入框滚动
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
console.log("滚动");
}
// 选中事件
function handleSelect(e: React.SyntheticEvent<HTMLTextAreaElement>) {
console.log("选中文本");
}
当不知道用什么类型时,悬停查看:鼠标放到 onChange
、onClick
等事件上,VSCode 会提示具体类型。
回退方案:用 React.SyntheticEvent
作为兜底类型。
function handleSomething(e: React.SyntheticEvent) {
console.log(e.type);
}
子元素类型
在 React 中,children
是组件非常常见的一个属性。根据场景不同,你可以选择不同的类型来描述它。
React.ReactNode
ReactNode
(最宽泛的定义)表示 可以在 JSX 中渲染的所有类型。包括:
string
、number
JSX.Element
(如<div />
)null
、undefined
boolean
(通常不会渲染)ReactFragment
(多个子元素<>...</>
)ReactPortal
interface ModalRendererProps {
title: string;
children: React.ReactNode;
}
function ModalRenderer({ title, children }: ModalRendererProps) {
return (
<div className="modal">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
使用场景:适用于几乎所有情况,是最常见的写法。
React.ReactElement
ReactElement
(限制为单个 JSX 元素) 更严格,它表示:
- 仅允许 JSX 元素
- 不允许
string
、number
等原始类型
interface ModalRendererProps {
title: string;
children: React.ReactElement;
}
function ModalRenderer({ title, children }: ModalRendererProps) {
return (
<div className="modal">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
}
// OK
<ModalRenderer title="Hello"><p>内容</p></ModalRenderer>
// 错误:不能传 string
<ModalRenderer title="Hello">文本内容</ModalRenderer>
使用场景:当你 明确需要 JSX 元素,而不是文本、数字时。
子元素数量
ReactElement | ReactElement[]
有时我们希望组件 只接收 JSX 元素,而且允许多个。
interface ListProps {
children: React.ReactElement | React.ReactElement[];
}
function List({ children }: ListProps) {
return <ul>{children}</ul>;
}
<List>
<li>Item 1</li>
<li>Item 2</li>
</List>;
自定义函数子组件
除了 ReactNode
/ ReactElement
,有时我们也会接收 函数作为 children:
interface DataProviderProps {
children: (data: string) => React.ReactNode;
}
function DataProvider({ children }: DataProviderProps) {
return <div>{children("Hello from provider")}</div>;
}
<DataProvider>
{(data) => <p>{data}</p>}
</DataProvider>;
使用场景:适合 高阶组件 / 逻辑封装 场景。
限制:你不能使用 TypeScript 来强制指定某种 HTML 元素
// 这是做不到的
interface ListProps {
children: React.ReactElement<HTMLLIElement>;
}
原因是:React 的类型系统无法区分具体的 JSX 标签(如 <li>
vs <div>
),它只认 ReactElement
。
如果你真的需要验证子元素类型,可以在 运行时(比如 React.Children.forEach
)里做检查,而不是靠 TS。
所以不能限制具体标签类型,只能在运行时校验
样式类型
在 React 中,如果你使用 内联样式,就可以利用 TypeScript 的类型系统来获得更好的提示与检查。
React.CSSProperties
React.CSSProperties
是 React 提供的类型,涵盖了 所有有效的 CSS 属性,并且能在编辑器中自动提示。
interface MyComponentProps {
style: React.CSSProperties;
}
function MyComponent({ style }: MyComponentProps) {
return <div style={style}>Hello</div>;
}
// 使用时会有属性提示
<MyComponent style={{ color: "red", fontSize: 20 }} />;
注意:数值类型的属性(如 fontSize
)会被自动推断为 px
,但你也可以显式加单位:
<MyComponent style={{ fontSize: "2rem" }} />
可选样式属性
通常我们不会要求组件 必须传递 style
,所以实际写法会是:
interface MyComponentProps {
style?: React.CSSProperties;
}
这样组件使用时就可以不写 style
。
扩展内联样式
有时我们会希望 style
能与其他属性结合:
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
style?: React.CSSProperties;
}
function Button({ style, ...rest }: ButtonProps) {
return <button style={{ padding: "8px 16px", ...style }} {...rest} />;
}
// 内置属性 + style 扩展
<Button style={{ backgroundColor: "blue", color: "white" }} onClick={() => alert("clicked")} />
限制某些样式属性
如果你只允许特定样式,可以手动挑选:
interface BoxProps {
style: Pick<React.CSSProperties, "width" | "height" | "backgroundColor">;
}
function Box({ style }: BoxProps) {
return <div style={style} />;
}
// 只能传 width / height / backgroundColor
<Box style={{ width: 100, height: 50, backgroundColor: "lightblue" }} />;
与 className 配合
在真实项目里,style
通常和 className
结合使用:
interface CardProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
function Card({ className, style, children }: CardProps) {
return (
<div className={`rounded-lg shadow ${className}`} style={style}>
{children}
</div>
);
}
推荐:结构化样式用 className(CSS/Tailwind),少量动态样式用 style。
贡献者
更新日志
dd260
-doc update于ffca1
-doc update于