
一句话:React + TS 的核心是「把 props、事件、children、ref 的类型写准」——基础组件要
extends React.XxxHTMLAttributes<T>才能透传原生属性,容器组件用泛型让类型跟着数据走。
一、Props 类型
// ✅ 方式一:interface(推荐——报错信息友好、可扩展)
interface UserCardProps {
name: string;
age?: number; // 可选
tags: string[];
onClick?: () => void;
children?: React.ReactNode;
}
function UserCard({ name, age = 0, tags, onClick, children }: UserCardProps) {
return (
<div onClick={onClick}>
{name} / {age} / {tags.length} / {children}
</div>
);
}
// ✅ 方式二:type(适合「联合、交叉、映射」等复杂类型)
type ButtonProps = {
variant: 'primary' | 'secondary';
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
// ✅ 方式三:互斥 props(用判别联合)
type AlertProps =
| { type: 'link'; href: string }
| { type: 'button'; onClick: () => void };
| 场景 | 推荐 |
|---|---|
| 普通组件 props | interface |
| 联合/交叉/条件类型 | type |
| 互斥 props | type + 判别联合 |
| 需要声明合并(扩展第三方类型) | interface |
// ✅ 小技巧:用「类型断言」保证「每个分支都被处理」
function assertNever(x: never): never {
throw new Error(`未处理的分支: ${JSON.stringify(x)}`);
}
function Alert(props: AlertProps) {
switch (props.type) {
case 'link':
return <a href={props.href}>链接</a>;
case 'button':
return <button onClick={props.onClick}>按钮</button>;
default:
return assertNever(props); // ⭐ 新增分支时这里会编译报错
}
}
二、children 的类型
// 三个常见选项(按「允许什么」从宽到严)
type A = { children: React.ReactNode }; // ⭐ 最常用(任何可渲染内容)
type B = { children: React.ReactElement }; // 只允许「单个元素」
type C = { children: React.ReactElement[] }; // 只允许「元素数组」
type D = { children: (x: Data) => React.ReactNode }; // 函数(render props)
| 类型 | 允许的内容 | 适合 |
|---|---|---|
React.ReactNode |
元素/字符串/数字/null/undefined/数组/Fragment |
大多数场景 |
React.ReactElement |
单个元素(不含文本、数组) | 「必须传一个元素」的容器 |
React.ReactElement[] |
元素数组 | 列表类容器 |
React.JSX.Element |
单个 JSX 元素(React 19 的新命名) | 同 ReactElement |
// ⚠️ React.ReactNode 的坑:它包含 undefined,所以「不传 children」也合法
interface Props { children: React.ReactNode }
<Comp /> // ✅ 合法(children 是 undefined)
// 如果「必须传」→ 用 React.ReactElement(不包含 undefined)
三、事件类型
// ✅ 常见的元素事件类型(用 React.XxxEventHandler<T> 或 React.XxxEvent<T>)
interface Props {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onFocus: (e: React.FocusEvent<HTMLElement>) => void;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
onScroll: (e: React.UIEvent<HTMLDivElement>) => void;
onPointerDown: (e: React.PointerEvent<HTMLElement>) => void;
onDragOver: (e: React.DragEvent<HTMLElement>) => void;
onAnimationEnd: (e: React.AnimationEvent<HTMLElement>) => void;
}
// ✅ 两个「EventHandler」写法(更简洁,不用写参数类型)
interface Props {
onClick: React.MouseEventHandler<HTMLButtonElement>;
onChange: React.ChangeEventHandler<HTMLInputElement>;
}
// ⚠️ 常见错误:直接用「原生事件类型」
onClick: (e: MouseEvent) => void; // ❌ 那是 DOM 的 MouseEvent,不是 React 的
onClick: (e: React.MouseEvent) => void; // ✅
// ✅ 与原生事件的转换
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.nativeEvent); // 原生事件(MouseEvent)
};
// ✅ 需要「读 DOM 属性」时的类型
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
const value = e.currentTarget.value; // e.currentTarget 有正确的类型
};
// ⚠️ 用 e.target 时类型是 EventTarget(更宽泛)→ 需要断言或用 currentTarget
| 常用事件 | 类型 |
|---|---|
| 点击 | React.MouseEvent<T> |
| 输入变化 | React.ChangeEvent<T> |
| 键盘 | React.KeyboardEvent<T> |
| 表单提交 | React.FormEvent<T> |
| 焦点 | React.FocusEvent<T> |
| 滚动 | React.UIEvent<T> |
| 指针 | React.PointerEvent<T> |
| 拖拽 | React.DragEvent<T> |
| 剪贴板 | React.ClipboardEvent<T> |
四、基础组件继承原生属性
// ✅ 关键:extends React.XxxHTMLAttributes<T> 才能透传原生属性
import { forwardRef, type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ variant = 'primary', size = 'md', className, children, ...rest },
ref
) {
return (
<button
ref={ref}
className={clsx('btn', `btn--${variant}`, `btn--${size}`, className)} {/* ⭐ 合并外部 className */}
{...rest} {/* ⭐ 透传其余原生属性 */}
>
{children}
</button>
);
});
// 使用:onClick / disabled / type / aria-* 等都有类型且有提示
<Button variant="primary" onClick={handleClick} disabled type="submit" aria-label="提交">
确定
</Button>;
// ⚠️ 三种「原生属性」类型的区别(基础组件常常搞混)
// ① HTMLAttributes:不含「各种元素特有的事件/属性」(如 value、href)
type A = React.HTMLAttributes<HTMLDivElement>;
// ② XxxHTMLAttributes:具体元素的属性(推荐!)
type B = React.ButtonHTMLAttributes<HTMLButtonElement>;
type C = React.InputHTMLAttributes<HTMLInputElement>;
type D = React.AnchorHTMLAttributes<HTMLAnchorElement>;
// ③ AllHTMLAttributes:所有 HTML 元素的属性(很宽泛,少用)
type E = React.AllHTMLAttributes<HTMLElement>;
| 类型 | 包含 | 适合 |
|---|---|---|
HTMLAttributes<T> |
通用属性(不含元素特有) | 纯布局容器(div 类) |
ButtonHTMLAttributes<T> |
button 的全部属性(含 disabled、type) |
button 封装 |
InputHTMLAttributes<T> |
input 的全部属性(含 value、onChange 类型) |
input 封装 |
AnchorHTMLAttributes<T> |
a 的属性(含 href、target) |
链接封装 |
AllHTMLAttributes<T> |
所有元素的属性 | 极少用 |
// ✅ 更「万能」的写法:让组件能渲染成「任意元素」(多态组件)
type PolymorphicProps<E extends React.ElementType, P = {}> = P & {
as?: E;
} & Omit<React.ComponentPropsWithoutRef<E>, keyof P>;
function Text<E extends React.ElementType = 'span'>({
as,
children,
...rest
}: PolymorphicProps<E, { color?: string }>) {
const Tag = as ?? 'span';
return <Tag {...rest}>{children}</Tag>;
}
// 使用:自动推导出「对应元素的属性」
<Text as="a" href="/x">链接</Text>; // ✅ href 有类型
<Text as="button" disabled>按钮</Text>; // ✅ disabled 有类型
五、泛型组件
// ✅ 泛型组件:类型跟着「数据」走(容器/列表类组件常用)
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyText?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyText = '暂无数据' }: ListProps<T>) {
if (items.length === 0) return <p>{emptyText}</p>;
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// ⭐ 使用时 T 会自动推导
<List
items={users} // T = User
renderItem={(u) => u.name} // ✅ u 有 User 类型
keyExtractor={(u) => u.id} // ✅
/>;
// ✅ 带约束的泛型(要求 T 必须有某个字段)
interface SelectProps<T extends { id: string; label: string }> {
options: T[];
value: string | null;
onChange: (option: T) => void;
}
function Select<T extends { id: string; label: string }>({ options, value, onChange }: SelectProps<T>) {
return (
<select value={value ?? ''} onChange={(e) => onChange(options.find((o) => o.id === e.target.value)!)}>
{options.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select>
);
}
// ✅ 泛型 + forwardRef(`<T,>` 的逗号是为了「避免被当成 JSX」)
const Select = <T extends { id: string },>(
props: SelectProps<T>,
ref: React.Ref<HTMLSelectElement>
) => { /* ... */ };
export const SelectWithRef = forwardRef(Select) as <T extends { id: string }>(
props: SelectProps<T> & { ref?: React.Ref<HTMLSelectElement> }
) => React.ReactElement;
六、常用工具类型
// ⭐ 从「已有组件」提取 props 类型(封装组件时最常用)
type ButtonProps = React.ComponentProps<typeof Button>;
type ButtonProps2 = React.ComponentPropsWithoutRef<typeof Button>; // 不含 ref
type ButtonProps3 = React.ComponentPropsWithRef<typeof Button>; // 含 ref
// ⭐ 带 children 的 props
type Props = React.PropsWithChildren<{ title: string }>;
// 等价于 { title: string; children?: React.ReactNode }
// ⭐ 把「组件」当类型用(多态组件的 as 属性)
type AsProps = { as: React.ElementType };
// ⭐ 提取「事件的参数类型」
type SubmitHandler = React.ComponentProps<'form'>['onSubmit'];
// ⭐ 提取「自定义 Hook 的返回值」
type UseFormReturn = ReturnType<typeof useForm>;
// ⭐ 从「已有类型」里挑选/排除
type FormProps = Pick<UserCardProps, 'name' | 'age'>;
type FormProps2 = Omit<UserCardProps, 'children'>;
// ⭐ 让「某一组属性必填」
type RequiredName = Required<Pick<UserCardProps, 'name'>> & UserCardProps;
| 工具类型 | 作用 |
|---|---|
React.ComponentProps<typeof C> |
提取组件的 props(含 ref) |
React.ComponentPropsWithoutRef<typeof C> |
提取 props(不含 ref) |
React.ComponentPropsWithRef<typeof C> |
提取 props(含 ref,显式) |
React.PropsWithChildren<P> |
给 P 加上可选 children |
React.ElementType |
「能当标签用的东西」(字符串或组件) |
ReturnType<typeof fn> |
提取函数返回值类型 |
Pick / Omit / Required / Partial |
内置的工具类型 |
七、ref 的类型
// ✅ React 18:forwardRef + 两个泛型参数
import { forwardRef, useRef } from 'react';
interface InputProps {
label: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(function Input({ label }, ref) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
});
const ref = useRef<HTMLInputElement>(null); // ⭐ 类型:HTMLInputElement | null
<Input label="用户名" ref={ref} />;
ref.current?.focus(); // ✅ 需要可选链(可能是 null)
// ✅ React 19:ref 作为普通 prop
interface InputProps {
label: string;
ref?: React.Ref<HTMLInputElement>; // ⭐ 显式声明
}
function Input({ label, ref }: InputProps) {
return (
<label>
{label}
<input ref={ref} />
</label>
);
}
// ✅ 暴露「命令式接口」时的 ref 类型
export interface FormHandle {
validate: () => boolean;
reset: () => void;
}
const Form = forwardRef<FormHandle, Props>(function Form(props, ref) {
useImperativeHandle(ref, () => ({ validate, reset }), []);
return <form>{props.children}</form>;
});
const formRef = useRef<FormHandle>(null);
<Form ref={formRef} />;
formRef.current?.validate();
| 场景 | ref 类型 |
|---|---|
| DOM 元素 | useRef<HTMLInputElement>(null) → HTMLInputElement \| null |
| 只读 DOM | useRef<HTMLDivElement>(null) |
| 自定义实例 | useRef<MyInstance \| null>(null) |
| 命令式接口 | useRef<FormHandle>(null) |
| 组件接收 ref | React.Ref<T> |
// ⚠️ useRef 的三种签名差异(容易踩坑)
const a = useRef<HTMLDivElement>(null); // RefObject<HTMLDivElement | null>(只读 current)
const b = useRef<number>(0); // MutableRefObject<number>(可写 current)
const c = useRef<number | null>(null); // 可写
// ✅ 规律:传「null 字面量」→ current 只读(用于 DOM ref);
// 传其它初始值 → current 可写(用于「可变的普通值」)
八、常见坑
// ① 事件类型写成「原生事件」
const handle = (e: MouseEvent) => {}; // ❌ 这是 DOM MouseEvent
const handle2 = (e: React.MouseEvent) => {}; // ✅
// ② 基础组件没有 extends HTMLAttributes → 原生属性没有类型
interface Props { onClick?: () => void } // ⚠️ 没有 disabled/type/aria-*
interface Props2 extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant: 'a' } // ✅
// ③ 忘了「展开 rest」→ 原生属性没被透传
<button className={className}>{children}</button> // ❌ 丢了 onClick/disabled
<button className={className} {...rest}>{children}</button> // ✅
// ④ 忘了「合并 className」(覆盖外部传入的样式)
<button className="btn" {...rest} /> // ⚠️ rest 里的 className 会覆盖
// ✅ 用 clsx 合并
// ⑤ 用 React.FC(不推荐:隐式 children、泛型难用)
const Comp: React.FC<Props> = ({ children }) => <div>{children}</div>; // ⚠️
function Comp2({ children }: Props) { return <div>{children}</div>; } // ✅
// ⑥ 泛型组件写成 React.FC → 泛型失效
// ⑦ 用 e.target.value 而不是 e.currentTarget.value(类型更宽泛)
// ⑧ useRef<HTMLDivElement>() 不传 null → current 类型不含 null(可能误用)
// ⑨ 把「可选 props」写成必填(下游会报错)
// ⑩ 用 any 抹掉类型(丢失检查)
const props: any = ...; // ❌
// ⑪ 用「字符串」传给枚举式 props(拼写错误无法发现)
<Alert type="succes" /> // ✅ 会编译报错(联合类型的好处)
// ⑫ 忘记「导出 props 类型」(下游无法复用)
export interface ButtonProps { ... } // ✅
// ⑬ 用「interface 定义联合类型」(写不出来)→ 用 type
// ⑭ 在「跨包」使用 props 类型时没加 export type
// ⑮ 用 `React.JSX.Element` 与 `React.ReactElement` 混用(React 19 起前者更标准)
// ⑤ 的正确写法(不推荐 React.FC 的原因)
// ❌ React.FC 的问题:
// · 隐式包含 children(即使你的组件不支持)
// · 泛型组件用 React.FC 会丢失泛型
// · 返回值被固定为 ReactElement(无法返回 string 等)
// ✅ 直接用「函数 + 显式 props 类型」
function Button({ variant, ...rest }: ButtonProps) { ... }
九、面试延伸
- 「React 组件的 props 类型怎么写?」
三种方式:①interface——大多数场景(报错信息友好、可扩展、支持声明合并);②type——联合/交叉/条件类型(如「互斥 props」用判别联合);③ 需要「透传原生属性」时用interface Props extends React.XxxHTMLAttributes<T>。规范上建议「导出 props 类型」(下游可复用)、不用React.FC(隐式 children、泛型难用)、枚举式 props 用联合类型(拼写错误能被编译发现)。 -
「基础组件(Button/Input)的 props 类型有什么讲究?」
关键是extends React.XxxHTMLAttributes<T>(而不是只用HTMLAttributes)——因为「元素特有属性」(如 button 的disabled/type、input 的value/onChange、a 的href)不在通用的HTMLAttributes里。同时组件内部要{...rest}透传、并用clsx合并外部的className(否则要么丢属性、要么覆盖外部样式)。这样封装出来的组件既有类型提示,又保持了原生元素的能力。 -
「
children的类型该怎么选?」
四个选项:①React.ReactNode(最常用)——允许元素、字符串、数字、null/undefined、数组;②React.ReactElement——只允许「单个元素」(不含undefined,所以「必须传 children」时用它);③React.ReactElement[]——只允许元素数组(列表容器);④ 函数类型——render props((data) => ReactNode)。注意ReactNode包含undefined,所以「不传 children」也是合法的。 -
「React 的事件类型怎么写?」
用React.XxxEvent<T>或更简洁的React.XxxEventHandler<T>:如React.MouseEvent<HTMLButtonElement>、React.ChangeEvent<HTMLInputElement>、React.KeyboardEvent<T>、React.FormEvent<HTMLFormElement>。注意不要写成原生事件类型(MouseEvent是浏览器的,不是 React 的)。读值时优先用e.currentTarget(它有正确类型;e.target的类型更宽泛,需要断言)。 -
「什么是泛型组件?有什么用?」
泛型组件指「类型参数由使用方推导」的组件(如function List<T>({ items, renderItem }: ListProps<T>))。它解决的问题是「容器组件的类型要跟着数据走」——如果没有泛型,renderItem的item参数只能写成any,丢失类型检查。典型场景:列表/表格(items+renderItem)、选择器(options+onChange)、表单项(value+onChange)。还可以给泛型加约束(T extends { id: string })。 -
「怎么从一个已有组件提取 props 类型?」
用React.ComponentProps<typeof Component>(含ref)、React.ComponentPropsWithoutRef<typeof Component>(不含ref)、React.ComponentPropsWithRef<typeof Component>(显式含ref)。这在「二次封装组件」时特别有用——比如你做了一个MyButton包装antd的Button,可以用React.ComponentProps<typeof AntButton>拿到它的全部 props 类型,再Omit掉你要覆盖的字段。相关工具还有PropsWithChildren、ElementType、ReturnType。 -
「
useRef的类型为什么有两种行为?」
因为 TypeScript 对useRef有重载:① 传null字面量时,返回RefObject<T>(current是只读的)——这是为「DOM ref」设计的(你不应该手动给 DOM ref 赋值);② 传其它初始值时,返回MutableRefObject<T>(current可写)——这是为「保存可变值」设计的。所以「存定时器 ID / 计数器」要用useRef<number>(0)(可写),而「取 DOM」用useRef<HTMLDivElement>(null)(只读current,由 React 赋值)。 -
「用 TypeScript 写 React 有哪些最佳实践?」
八条:① 导出组件的 props 类型(便于复用与测试);② 不用React.FC(隐式 children、泛型问题);③ 基础组件extends XxxHTMLAttributes并透传...rest、用clsx合并className;④ 事件用React.XxxEvent(不是原生类型);⑤ 枚举式 props 用联合类型(拼写错误能编译发现);⑥ 用判别联合表达「互斥 props」,并用assertNever保证「穷尽处理」;⑦ 容器组件用泛型(类型跟着数据);⑧ 避免any(用unknown+ 类型收窄代替)。
一句话速记
props 类型用
interface(复杂类型用type),基础组件必须extends React.XxxHTMLAttributes<T>(否则disabled/value/href这类「元素特有属性」没有类型),并{...rest}透传 +clsx合并className;children首选React.ReactNode(含undefined,要「必填」就用React.ReactElement);事件用React.MouseEvent<T>/React.ChangeEvent<T>(不要写原生MouseEvent,取值优先e.currentTarget);泛型组件让类型跟着items/options走(列表、选择器、表单);提取已有组件的 props 用React.ComponentProps<typeof C>(不含 ref 用WithoutRef);useRef传null→current只读(DOM ref)、传其它值 → 可写(存可变值);规范上导出 props 类型、不用React.FC、枚举 props 用联合类型、互斥 props 用判别联合 +assertNever、避免any。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!