React 与 TypeScript
TypeScript 是 JavaScript 的超集,它添加了静态类型检查,能在开发阶段发现潜在错误。React 与 TypeScript 的结合已经成为现代前端开发的标准配置,能够显著提高代码的可维护性和开发体验。
为什么要在 React 中使用 TypeScript?
类型安全的好处
- 编译时错误检测:在代码运行前发现类型错误
- 更好的 IDE 支持:自动补全、重构工具、类型提示
- 代码即文档:类型定义本身就是最好的文档
- 更安全的重构:类型系统帮助确保重构不会遗漏任何地方
- 团队协作:明确的接口定义减少沟通成本
常见类型错误示例
// TypeScript 能在编译时捕获这些错误
// 错误:将字符串传递给期望数字的函数
function add(a: number, b: number): number {
return a + b;
}
add(1, '2'); // TypeScript 报错
// 错误:访问可能不存在的属性
interface User {
name: string;
}
const user: User = { name: '张三' };
user.age; // TypeScript 报错:属性 'age' 不存在
// 错误:遗漏必需的 props
interface ButtonProps {
label: string;
onClick: () => void;
}
function Button({ label }: ButtonProps) { /* ... */ }
<Button label="点击" /> // TypeScript 报错:缺少 onClick
组件类型定义
函数组件的类型
在 TypeScript 中定义 React 函数组件有多种方式:
import type { FC, ReactNode } from 'react';
// 方式 1:直接函数声明(推荐)
interface GreetingProps {
name: string;
}
function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}</h1>;
}
// 方式 2:使用 FC 类型
const Greeting: FC<GreetingProps> = ({ name }) => {
return <h1>Hello, {name}</h1>;
};
// 方式 3:使用箭头函数
const Greeting = ({ name }: GreetingProps) => {
return <h1>Hello, {name}</h1>;
};
推荐使用方式
现代 React 开发推荐使用直接函数声明的方式,原因如下:
// ✅ 推荐:直接函数声明
function MyComponent({ title, count }: MyComponentProps) {
return <div>{title}: {count}</div>;
}
// ⚠️ FC 类型有一些缺点
const MyComponent: FC<MyComponentProps> = ({ title, count }) => {
return <div>{title}: {count}</div>;
};
// FC 会自动添加 children 类型,但 React 18+ 已不再推荐隐式 children
// FC 无法很好地支持泛型组件
Props 类型定义
// 基础 Props
interface UserCardProps {
name: string;
age: number;
isActive?: boolean; // 可选属性
}
function UserCard({ name, age, isActive = false }: UserCardProps) {
return (
<div>
<h3>{name}</h3>
<p>年龄: {age}</p>
<p>状态: {isActive ? '活跃' : '离线'}</p>
</div>
);
}
// 使用
<UserCard name="张三" age={25} />
<UserCard name="李四" age={30} isActive />
children 类型
import type { ReactNode, ReactElement } from 'react';
// 接收任意 React 内容
interface CardProps {
title: string;
children: ReactNode; // 可以是字符串、数字、元素、数组等
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="content">{children}</div>
</div>
);
}
// 只接收 React 元素
interface ButtonGroupProps {
children: ReactElement | ReactElement[];
}
// 使用函数作为 children(render props)
interface ListProps<T> {
items: T[];
children: (item: T, index: number) => ReactNode;
}
function List<T>({ items, children }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{children(item, index)}</li>
))}
</ul>
);
}
// 使用
<List items={[1, 2, 3]}>
{(item, index) => <span>Item {index}: {item}</span>}
</List>
事件处理器类型
import type {
MouseEvent,
KeyboardEvent,
ChangeEvent,
FormEvent,
FocusEvent
} from 'react';
interface FormProps {
onSubmit: (e: FormEvent<HTMLFormElement>) => void;
}
function MyForm({ onSubmit }: FormProps) {
// 鼠标事件
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
console.log('Button clicked');
};
// 键盘事件
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
// 表单变化事件
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// 焦点事件
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
console.log('Input focused');
};
return (
<form onSubmit={onSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} onFocus={handleFocus} />
<button onClick={handleClick}>提交</button>
</form>
);
}
useState 类型
类型推断
TypeScript 通常能自动推断 useState 的类型:
// 自动推断为 number
const [count, setCount] = useState(0);
// 自动推断为 string
const [name, setName] = useState('张三');
// 自动推断为 boolean[]
const [items, setItems] = useState([true, false, true]);
显式类型声明
在某些情况下需要显式指定类型:
// 初始值为 null 或 undefined
const [user, setUser] = useState<User | null>(null);
const [data, setData] = useState<string | undefined>(undefined);
// 复杂对象类型
interface User {
id: number;
name: string;
email: string;
}
const [user, setUser] = useState<User>({ id: 0, name: '', email: '' });
// 联合类型
type Status = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState<Status>('idle');
// 对象字典
const [cache, setCache] = useState<Record<string, User>>({});
// 数组类型
const [users, setUsers] = useState<User[]>([]);
函数式更新
const [count, setCount] = useState(0);
// TypeScript 知道 prev 是 number 类型
setCount(prev => prev + 1);
// 复杂对象更新
const [user, setUser] = useState<User>({ id: 1, name: '张三', email: '[email protected]' });
setUser(prev => ({
...prev,
name: '李四'
}));
惰性初始化
// 初始值需要计算时,使用函数初始化
const [state, setState] = useState(() => {
const saved = localStorage.getItem('key');
return saved ? JSON.parse(saved) : defaultValue;
});
// 需要显式类型
const [data, setData] = useState<User | null>(() => {
return fetchInitialData();
});
useRef 类型
DOM 元素引用
import { useRef, useEffect } from 'react';
function InputComponent() {
// 显式指定 HTMLInputElement 类型
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 需要检查 null
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;
}
// 常见 DOM 元素类型
const divRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const videoRef = useRef<HTMLVideoElement>(null);
const formRef = useRef<HTMLFormElement>(null);
可变值引用
// 存储任意可变值,修改不会触发重渲染
const timerRef = useRef<number | null>(null);
const countRef = useRef<number>(0);
function startTimer() {
timerRef.current = window.setInterval(() => {
countRef.current++;
console.log(countRef.current);
}, 1000);
}
function stopTimer() {
if (timerRef.current !== null) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}
forwardRef 组件
import { forwardRef } from 'react';
// 定义 Props 和 Ref 类型
interface InputProps {
label: string;
type?: string;
}
// forwardRef 接受两个泛型参数:Ref 类型 和 Props 类型
const FancyInput = forwardRef<HTMLInputElement, InputProps>(
({ label, type = 'text' }, ref) => {
return (
<div className="input-wrapper">
<label>{label}</label>
<input ref={ref} type={type} />
</div>
);
}
);
// 使用
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<FancyInput ref={inputRef} label="用户名" />
);
}
// React 19 新语法:ref 作为 prop
// 无需 forwardRef
function FancyInput({ label, type = 'text', ref }: InputProps & { ref?: Ref<HTMLInputElement> }) {
return (
<div className="input-wrapper">
<label>{label}</label>
<input ref={ref} type={type} />
</div>
);
}
useContext 类型
定义带类型的 Context
import { createContext, useContext, useState } from 'react';
// 定义 Context 值类型
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// 创建 Context,提供默认值或 undefined
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
// 自定义 Hook,提供类型安全的访问
function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Provider 组件
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const value: ThemeContextValue = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
// 使用
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{ background: theme === 'dark' ? '#333' : '#fff' }}
>
Toggle Theme
</button>
);
}
React 19 简化语法
// React 19 支持直接使用 Context 作为 Provider
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
<ThemeContext value={{ theme, toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}>
{children}
</ThemeContext>
);
}
useReducer 类型
import { useReducer } from 'react';
// 定义状态类型
interface State {
count: number;
loading: boolean;
error: string | null;
}
// 定义 Action 类型(联合类型)
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET_VALUE'; payload: number }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null };
// Reducer 函数
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET_VALUE':
return { ...state, count: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
default:
return state;
}
}
// 使用 useReducer
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
loading: false,
error: null
});
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'SET_VALUE', payload: 10 })}>
Set to 10
</button>
</div>
);
}
事件类型速查表
常用事件类型
import type {
// 表单事件
ChangeEvent, // onChange
FormEvent, // onSubmit
InputEvent, // onInput
// 鼠标事件
MouseEvent, // onClick, onMouseDown, onMouseUp, onMouseEnter, onMouseLeave
// 键盘事件
KeyboardEvent, // onKeyDown, onKeyUp, onKeyPress
// 焦点事件
FocusEvent, // onFocus, onBlur
// 拖拽事件
DragEvent, // onDrag, onDrop
// 剪贴板事件
ClipboardEvent, // onCopy, onPaste, onCut
// 触摸事件
TouchEvent, // onTouchStart, onTouchMove, onTouchEnd
// 滚动事件
UIEvent, // onScroll
// 动画事件
AnimationEvent, // onAnimationStart, onAnimationEnd
// 过渡事件
TransitionEvent // onTransitionEnd
} from 'react';
// 使用示例
function EventExamples() {
// 表单提交
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
// 输入变化
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// 鼠标点击
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
// 键盘事件
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
}
};
// 焦点事件
const handleFocus = (e: FocusEvent<HTMLInputElement>) => {
console.log('Focused');
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} onFocus={handleFocus} />
<button onClick={handleClick}>提交</button>
</form>
);
}
select 和 textarea 类型
// select 元素
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value);
};
// textarea 元素
const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value);
};
泛型组件
当组件需要处理多种类型时,可以使用泛型:
// 泛型列表组件
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
keyExtractor: (item: T, index: number) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item, index)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用
interface User {
id: number;
name: string;
}
<List<User>
items={[
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
// 泛型表格组件
interface TableProps<T> {
data: T[];
columns: {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => ReactNode;
}[];
}
function Table<T extends object>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map(col => (
<th key={String(col.key)}>{col.header}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render
? col.render(row[col.key], row)
: String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
工具类型
Partial 和 Required
interface User {
id: number;
name: string;
email: string;
}
// Partial:所有属性变为可选
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
// Required:所有属性变为必需
type CompleteUser = Required<User>;
Pick 和 Omit
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Pick:选择部分属性
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string; }
// Omit:排除部分属性
type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; }
组件 Props 类型提取
// 提取组件 Props 类型
type MyComponentProps = ComponentProps<typeof MyComponent>;
// 提取 HTML 元素属性
type ButtonProps = ComponentProps<'button'>;
// 扩展 HTML 元素属性
interface CustomButtonProps extends ComponentProps<'button'> {
variant?: 'primary' | 'secondary';
}
function CustomButton({ variant = 'primary', ...props }: CustomButtonProps) {
return (
<button
{...props}
className={`btn btn-${variant}`}
/>
);
}
类型声明文件
对于无法找到类型声明的第三方库,可以创建类型声明文件:
// types/my-library.d.ts
declare module 'my-library' {
export interface Options {
timeout?: number;
retries?: number;
}
export function init(options?: Options): void;
export function destroy(): void;
}
小结
- 函数组件类型:推荐使用直接函数声明而非
FC - Props 类型:使用
interface定义,可选属性用? - 事件类型:使用
React.XXXEvent<ElementType>格式 - useState 类型:自动推断或显式指定泛型
- useRef 类型:DOM 引用用
HTMLElement类型 - useContext 类型:创建类型安全的 Context 和 Hook
- 泛型组件:处理多种类型的可复用组件
- 工具类型:
Partial,Pick,Omit,ComponentProps
练习
- 为一个表单组件定义完整的 TypeScript 类型
- 创建一个泛型选择器组件(Select)
- 实现一个类型安全的 useLocalStorage Hook
- 为一个复杂的 Context 创建完整的类型定义
- 使用
ComponentProps扩展一个 HTML 元素