写 React 用 TypeScript,不是为了写更多代码,是为了让编辑器替你找 bug。这篇文档只覆盖你写 React 每天都会遇到的 TS 语法,按出现频率排序。
前置:先读完 React 必备 JS 语法速查,这篇在其基础上补 TS 的部分。
一、类型注解
给变量加一个”标签”,告诉 TS 它是什么类型。
1.1 基本类型
const name: string = "yi";
const age: number = 25;
const isActive: boolean = true;
const items: string[] = ["a", "b"]; // 字符串数组
const nums: number[] = [1, 2, 3]; // 数字数组
const tuple: [string, number] = ["yi", 25]; // 固定长度+类型的数组(元组)
后端出身的话,这跟 Java 的 String name = "yi" 或 Python 的类型注解 name: str = "yi" 一样——声明类型,编译器帮你检查。
1.2 对象类型:内联写法
const user: { name: string; age: number } = {
name: "yi",
age: 25,
};
用分号 ; 分隔字段(在类型定义里),跟 JS 对象用逗号不同。
问题:对象类型写长了很丑,而且同一个类型可能多处复用 → 用接口。
二、接口 Interface
定义对象的”形状”——有哪些字段,每个字段什么类型。
2.1 基本用法
interface User {
name: string;
age: number;
email: string;
}
const user: User = { name: "yi", age: 25, email: "yi@test.com" };
比内联写法清晰,而且可以复用。
2.2 React 里的样子——Props 类型
对 Props 本身还不熟的话,先看博客中的《Props 完全指南》。
interface TodoListProps {
items: Todo[];
onDelete: (id: number) => void;
}
function TodoList({ items, onDelete }: TodoListProps) {
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => onDelete(item.id)}>删除</button>
</li>
))}
</ul>
);
}
每个组件的 Props 都用 interface 定义,这是 React + TS 的标准写法。
拆解:函数签名是三件事叠在一起
function TodoList({ items, onDelete }: TodoListProps);
| 步骤 | 写法 | 对应概念 |
|---|---|---|
| 1. 接收一个对象参数 | props | JS 函数参数 |
| 2. 解构出需要的属性 | { items, onDelete } | JS 解构赋值 |
| 3. 告诉 TS 这个对象什么形状 | : TodoListProps | TS 类型注解 |
Props 只有两种东西:数据 + 回调
interface TodoListProps {
items: Todo[]; // 数据:Todo 对象的数组
onDelete: (id: number) => void; // 回调:点删除时调用的函数
}
后端类比:items: Todo[] → List<Todo> items,就是传入数据;onDelete → Consumer<Integer> onDelete,就是传入回调函数(Java 的函数式接口 / Go 的 func 参数)。
事件处理:传函数,不传调用结果
// ❌ 错误:立即执行,一渲染就调用了
<button onClick={onDelete(item.id)}>
// ✅ 正确:包一层箭头函数,点击时才执行
<button onClick={() => onDelete(item.id)}>
跟 JS 速查里讲的同一个道理——事件处理函数必须传函数本身,不能传函数调用的结果。
2.3 React 里的样子——API 响应类型
interface ApiResponse {
code: number;
data: User[];
message: string;
}
// 后端接口返回的数据,用接口约束
async function fetchUsers(): Promise<ApiResponse> {
const res = await fetch("/api/users");
return res.json();
}
跟后端定义 DTO/VO 一个道理——前后端对齐数据结构。
2.4 interface vs type——哪个?
// interface:定义对象形状
interface User {
name: string;
}
// type:也能定义对象形状
type User = {
name: string;
};
90% 的情况用哪个都行。实际区别:
interface | type | |
|---|---|---|
| 可以继承 | extends | & 交叉 |
| 可以声明合并 | ✅ 同名自动合并 | ❌ 重复定义报错 |
| 能写联合类型 | ❌ | ✅ type A = B | C |
| 能写基本类型别名 | ❌ | ✅ type ID = string |
React 项目惯例:对象用 interface,其他用 type。不用纠结,团队统一即可。
三、可选属性 ?
属性名后面加 ?,表示”这个字段可以没有”。
3.1 定义
interface User {
name: string;
age: number;
email?: string; // 可选,可以不传
phone?: string; // 可选,可以不传
}
// ✅ 不传 email 和 phone 也行
const user: User = { name: "yi", age: 25 };
跟 JS 解构默认值的关系:
// TS 层面:email 是可选的(可以不传)
interface User {
email?: string;
}
// JS 层面:解构时给默认值(没传就用默认值)
const { email = "none" } = user;
3.2 React 里的样子——可选 Props
interface ButtonProps {
children: React.ReactNode;
variant?: "primary" | "secondary"; // 可选,默认 'primary'
size?: "sm" | "md" | "lg"; // 可选,默认 'md'
onClick?: () => void; // 可选回调
}
function Button({
children,
variant = "primary",
size = "md",
onClick,
}: ButtonProps) {
// ...
}
可选 Prop + 解构默认值,是 React 组件的标准模式。
四、联合类型 |
“这个值可以是 A,也可以是 B”。
4.1 基本用法
// 变量可以是多种类型
let id: string | number = "abc";
id = 123; // ✅ 也行
// 函数参数可以是多种类型
function formatId(id: string | number): string {
return String(id);
}
后端类比:Java 的泛型上界 <? extends A> 或 Go 的 interface{} + 类型断言,但 TS 的联合类型更精确——你明确列出所有可能的类型。
4.2 字面量联合——最实用的模式
限定值为几个固定选项:
type Status = "loading" | "success" | "error";
type Method = "GET" | "POST" | "PUT" | "DELETE";
type Theme = "light" | "dark";
const status: Status = "loading"; // ✅
const status2: Status = "pending"; // ❌ 不是允许的值
后端类比:枚举。但 TS 的字面量联合更轻量,不需要定义 enum 关键字,直接写字面值就行。
4.3 React 里的样子
// 组件的 variant 只允许这几个值
interface AlertProps {
variant: "info" | "warning" | "error";
message: string;
}
function Alert({ variant, message }: AlertProps) {
// TS 知道 variant 只能是这三个值,编辑器有自动补全
const colors = {
info: "blue",
warning: "yellow",
error: "red",
};
return <div style={{ color: colors[variant] }}>{message}</div>;
}
五、类型推断——不写类型也行
TS 很聪明,很多时候不用你写类型,它自己能猜到。
5.1 自动推断
// TS 推断出 name 是 string
const name = "yi";
// TS 推断出 age 是 number
const age = 25;
// TS 推断出 items 是 string[]
const items = ["a", "b", "c"];
// TS 推断出返回值是 number
function double(n: number) {
return n * 2; // 不用写 : number,TS 知道
}
5.2 什么时候必须写类型?
// ❌ 不写类型,TS 推断为 any(等于没类型)
let result;
result = fetchUser();
// ✅ 必须写类型
let result: User | null = null;
// ❌ 函数参数无法推断,必须写
function greet(name) {} // 报错:参数隐式有 'any' 类型
// ✅ 参数必须写类型
function greet(name: string) {}
原则:能推断就不写,推断不了或推断为 any 就必须写。函数参数永远要写。
5.3 React 里的样子
// useState 的初始值能推断类型,不用手写
const [count, setCount] = useState(0); // 推断为 number
const [name, setName] = useState(""); // 推断为 string
const [isOpen, setIsOpen] = useState(false); // 推断为 boolean
// 初始值是 null 或 undefined 时,必须手写类型
const [user, setUser] = useState<User | null>(null);
// ^^^^^^^^^^^^^ 必须写,否则推断为 null(永远赋不了别的值)
六、泛型 <T>
“类型的参数”——写一次类型定义,用在多种具体类型上。
6.1 为什么要泛型?
// 不用泛型:每种类型写一个函数
function firstString(arr: string[]): string {
return arr[0];
}
function firstNumber(arr: number[]): number {
return arr[0];
}
// 用泛型:写一次,所有类型都能用
function first<T>(arr: T[]): T {
return arr[0];
}
first<string>(["a", "b"]); // → string
first<number>([1, 2, 3]); // → number
// 大多数时候不用手写 <string>,TS 能推断
first(["a", "b"]); // TS 自动推断 T = string
后端类比:Java 的 List<T>、Go 的泛型 func First[T any](arr []T) T,一个意思。
6.2 React 里的样子
// useState 是泛型函数
const [count, setCount] = useState<number>(0)
// useState 的签名大概是:function useState<T>(initial: T): [T, (v: T) => void]
// useQuery 是泛型函数——告诉它返回的数据是什么类型
const { data } = useQuery<User>({
queryKey: ['user'],
queryFn: () => fetch('/api/user').then(res => res.json())
})
// 现在 data 的类型是 User | undefined,编辑器能补全 user.name、user.age
// 不写泛型 → data 类型是 unknown,什么属性都点不出来
const { data } = useQuery({ ... })
// data 是 unknown ❌
关键:useQuery<User> 就是告诉 TS “这个接口返回的数据是 User 类型”,之后 data 就有完整的类型提示了。这跟你后端定义接口返回类型一个道理。
七、类型断言 as
“我比你(TS)更清楚这个值的类型”。
7.1 基本用法
// fetch 返回 any,你知道它是 User
const data = res.json() as User;
// 事件对象的类型
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; // TS 知道 value 是 string
};
7.2 什么时候用 as?
// ✅ 你确实比 TS 知道更多信息时
const data = res.json() as User[];
// ✅ DOM 操作时
const input = document.getElementById("myInput") as HTMLInputElement;
input.value; // 没有 as 的话,TS 不知道这是 input 元素
// ❌ 用来绕过类型错误(跟 as any 一样危险)
const user = {} as User; // 骗过编译器,运行时可能出错
注意:as 不做任何运行时转换,只是编译期的类型标注。as User 不会把数据变成 User,只是告诉 TS “请当它是 User”。如果实际数据不是 User,运行时照样报错。
7.3 as 和后端类型转换的区别
// Java:强转,运行时检查,类型不对抛 ClassCastException
User user = (User) obj;
// TS:编译期标注,运行时完全消失,不做检查
const user = obj as User
TS 的 as 更像是”信任我”,不是”帮我转换”。
7.4 React 里的样子
// 事件处理——最常见的 as 场景
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
// 不用 as 的写法(更推荐)——直接在参数上标注类型
<input onChange={e => setValue(e.target.value)} />;
// TS 自动推断 e 的类型,不需要 as
八、工具类型
TS 内置的类型转换函数——把一个类型变成另一个类型。
8.1 Partial<T>——所有属性变可选
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
// 等价于:
// {
// name?: string
// age?: number
// email?: string
// }
React 场景——更新对象时,只传要改的字段:
function updateUser(id: number, updates: Partial<User>) {
// 只需要传要改的字段,不用传整个 User
}
updateUser(1, { age: 26 }); // ✅ 只改 age
8.2 Pick<T, K>——只取几个属性
type UserPreview = Pick<User, "name" | "age">;
// 等价于:
// {
// name: string
// age: number
// }
React 场景——组件只需要用部分字段:
interface UserCardProps {
user: Pick<User, "name" | "email">;
}
8.3 Omit<T, K>——排除几个属性
type UserWithoutEmail = Omit<User, "email">;
// 等价于:
// {
// name: string
// age: number
// }
React 场景——基于已有类型但去掉某些字段:
// 基础 Props,但不需要 children
type ButtonBaseProps = Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"children"
>;
8.4 Record<K, V>——键值对对象
// key 是 string,value 是 number
const scores: Record<string, number> = {
math: 90,
english: 85,
};
// 后端类比:Map<String, Integer>
React 场景——路由映射、主题配置:
const themeColors: Record<"light" | "dark", string> = {
light: "#ffffff",
dark: "#000000",
};
8.5 速查
| 工具类型 | 作用 | 后端类比 |
|---|---|---|
Partial<T> | 所有属性变可选 | 更新 DTO(字段均可选) |
Pick<T, K> | 只取某几个属性 | 视图对象(只暴露部分字段) |
Omit<T, K> | 排除某几个属性 | 创建 DTO(去掉 id/timestamp) |
Record<K, V> | 键值对映射 | Map<K, V> |
九、函数类型
给函数参数和返回值标注类型。
9.1 基本写法
// 参数类型 + 返回值类型
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数
const add = (a: number, b: number): number => a + b;
// 返回值通常可以省略,TS 能推断
const add = (a: number, b: number) => a + b; // TS 知道返回 number
9.2 回调函数类型
React 里到处都是回调,需要知道怎么标注类型:
// 无参数无返回值
const onClick: () => void = () => {};
// 有参数
const onChange: (value: string) => void = value => {};
// 作为 Props 的一部分
interface ButtonProps {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
}
9.3 React 常用事件类型
不用背,编辑器会提示,知道有这些就行:
| 事件 | 类型 |
|---|---|
| 点击 | React.MouseEvent<HTMLButtonElement> |
| 输入变化 | React.ChangeEvent<HTMLInputElement> |
| 表单提交 | React.FormEvent<HTMLFormElement> |
| 键盘 | React.KeyboardEvent<HTMLInputElement> |
实际写法——让 TS 自动推断,不用手写:
// ✅ 推荐:TS 自动推断 e 的类型
<button onClick={(e) => console.log(e.currentTarget)}>Click</button>
// ❌ 多此一举:手动标注
<button onClick={(e: React.MouseEvent<HTMLButtonElement>) => { }}>Click</button>
十、字面量类型 & as const
10.1 字面量类型——比 string 更精确
// 普通类型:太宽泛
let direction: string = "left";
direction = "any string"; // ✅ 不会报错,但可能是 typo
// 字面量类型:精确到某个值
let direction: "left" | "right" | "up" | "down" = "left";
direction = "right"; // ✅
direction = "diagonal"; // ❌ 不是允许的值
联合字面量类型 = 轻量级枚举。React 里比 enum 常用得多。
10.2 as const——让 TS 把值当作字面量
// 普通 const,TS 推断为 string[]
const ROLES = ["admin", "editor", "viewer"];
// ROLES 的类型是 string[]
// 加 as const,TS 推断为只读字面量元组
const ROLES = ["admin", "editor", "viewer"] as const;
// ROLES 的类型是 readonly ['admin', 'editor', 'viewer']
// 好处:可以从 ROLES 提取联合类型
type Role = (typeof ROLES)[number];
// 'admin' | 'editor' | 'viewer'
10.3 React 里的样子
// 定义状态机的所有状态
const STATUSES = ["idle", "loading", "success", "error"] as const;
type Status = (typeof STATUSES)[number];
interface State {
status: Status; // 只能是上面四个值之一
}
// 定义路由配置
const ROUTES = {
home: "/",
login: "/login",
dashboard: "/dashboard",
} as const;
// 每个值都是字面量类型,不是 string
速查表
| 语法 | 一句话 | React 里怎么用 |
|---|---|---|
: 类型 | 给变量加类型标签 | const [user, setUser] = useState<User | null>(null) |
interface | 定义对象形状 | Props 类型、API 响应类型 |
? 可选属性 | 这个字段可以没有 | 可选 Props、可选回调 |
| 联合类型 | 可以是 A 也可以是 B | variant: 'primary' | 'secondary' |
| 类型推断 | TS 自己猜,不用你写 | useState(0) 自动推断 number |
<T> 泛型 | 类型的参数 | useQuery<User>() 告诉 TS 数据类型 |
as 断言 | 我比 TS 更清楚类型 | res.json() as User |
Partial<T> | 全部变可选 | 更新函数的参数 |
Pick<T, K> | 只取几个属性 | 组件只需要部分字段 |
Omit<T, K> | 排除几个属性 | 去掉不需要的字段 |
as const | 值当字面量 | 状态常量、路由配置 |