TypeScript 已成为 React 项目的标配。本文不是基础教程,而是覆盖生产环境中最常用的类型模式、设计规范和常见陷阱的深度指南。
一、基础但关键的类型模式
1.1 Props 类型设计原则
// ✅ 使用 interface(可扩展,merge 友好)
interface ButtonProps {
children: React.ReactNode; // 通用子元素
variant?: 'primary' | 'secondary' | 'ghost'; // 字面量联合(有限选项)
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
className?: string; // 允许外部 className 覆盖
}
// ✅ HTML 原生属性继承(避免重复声明)
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
function Input({ label, error, className, ...rest }: InputProps) {
return (
<div className={className}>
<label>{label}</label>
<input {...rest} aria-invalid={!!error} />
{error && <span role="alert">{error}</span>}
</div>
);
}
Props 命名规范:
- 用
Props后缀:ButtonProps、CardProps - 不要内联类型:
function Button(props: { label: string })❌ - 不要把组件名放进接口:
interface Button { label: string }❌(会和 DOM Button 冲突)
1.2 组件返回类型
// ✅ 推荐:不声明返回类型,让 TypeScript 推断
function Button({ label }: ButtonProps) {
return <button>{label}</button>;
}
// ✅ 如果需要显式声明(如 forwardRef、HOC)
const Button: React.FC<ButtonProps> = ({ label }) => {
return <button>{label}</button>;
};
// ✅ forwardRef 类型
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...rest }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...rest} />
</div>
);
}
);
Input.displayName = 'Input';
React.FC 争议:社区推荐不使用
React.FC(不自动包含 children、无法正确支持泛型组件)。上面的示例仅为演示。推荐直接声明函数,infer 返回类型。
二、泛型组件:组件库的核心能力
2.1 List/Table 通用组件
interface ListProps<T> {
items: T[];
keyExtractor: (item: T) => string | number;
renderItem: (item: T, index: number) => React.ReactNode;
emptyComponent?: React.ReactNode;
className?: string;
}
function List<T>({ items, keyExtractor, renderItem, emptyComponent, className }: ListProps<T>) {
if (items.length === 0) {
return <>{emptyComponent ?? null}</>;
}
return (
<ul className={className}>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// 使用:类型自动推断
interface User {
id: string;
name: string;
}
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
emptyComponent={<p>No users found</p>}
/>
2.2 Select/Dropdown 泛型组件
interface SelectProps<T> {
options: T[];
value: T | null;
onChange: (value: T) => void;
labelKey: keyof T; // { label: string, value: string } 中取哪个字段当 label
valueKey: keyof T; // 取哪个字段当 value
placeholder?: string;
}
function Select<T extends Record<string, unknown>>({
options,
value,
onChange,
labelKey,
valueKey,
placeholder,
}: SelectProps<T>) {
return (
<select
value={value ? String(value[valueKey]) : ''}
onChange={(e) => {
const selected = options.find((o) => String(o[valueKey]) === e.target.value);
if (selected) onChange(selected);
}}
>
{placeholder && <option value="">{placeholder}</option>}
{options.map((option) => (
<option key={String(option[valueKey])} value={String(option[valueKey])}>
{String(option[labelKey])}
</option>
))}
</select>
);
}
2.3 as 属性:多态组件(Polymorphic)
// ✅ Button 组件可以渲染为 <button>、<a>、<Link> 等不同标签
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
} & Omit<React.ComponentPropsWithoutRef<E>, 'as'>;
function Button<C extends React.ElementType = 'button'>({
as,
children,
className,
...rest
}: PolymorphicProps<C>) {
const Component = as || 'button';
return (
<Component className={cn('btn', className)} {...rest}>
{children}
</Component>
);
}
// 使用
<Button>Default Button</Button> // <button>
<Button as="a" href="/login">Link Button</Button> // <a>
<Button as={Link} to="/home">React Router</Button> // <Link>
多态组件(Polymorphic)是 Radix UI、Headless UI 等组件库的核心模式,允许同一组件适配不同的底层 HTML 标签。
三、Hooks 的类型化
3.1 常用 Hook 类型速查
// useState:泛型显式声明
const [user, setUser] = useState<User | null>(null);
const [count, setCount] = useState<number>(0); // 可省略,自动推断为 number
// useRef:DOM 引用必须初始化 null
const inputRef = useRef<HTMLInputElement>(null);
// 若不加 null,HTMLElement 类型不匹配
// useMemo:返回值自动推断
const sortedUsers = useMemo(() =>
[...users].sort((a, b) => a.name.localeCompare(b.name)),
[users]
);
// useCallback:函数签名类型
const handleClick = useCallback<(id: string) => void>((id) => {
navigate(`/user/${id}`);
}, [navigate]);
// useReducer:状态与 Action 类型
interface State {
loading: boolean;
data: Post[] | null;
error: Error | null;
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: Post[] }
| { type: 'FETCH_ERROR'; payload: Error };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'FETCH_START': return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS': return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload };
default: return state;
}
};
3.2 自定义 Hook 的类型设计
// ✅ 自定义 Hook 返回对象时,使用接口而不是元组
interface UseFetchResult<T> {
data: T | undefined;
isLoading: boolean;
error: Error | null;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const refetch = useCallback(async () => {
setIsLoading(true);
try {
const res = await fetch(url);
const json = await res.json();
setData(json);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error(String(err)));
} finally {
setIsLoading(false);
}
}, [url]);
useEffect(() => { refetch(); }, [refetch]);
return { data, isLoading, error, refetch };
}
// 使用
const { data: user, isLoading } = useFetch<User>('/api/user/123');
3.3 Context 类型安全
// ✅ 创建 Context 时提供完备的类型
interface AuthContextType {
user: User | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
// ✅ 自定义 hook 处理 null check,避免每个组件都判断
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// 组件中使用
function Profile() {
const { user, logout } = useAuth(); // user 类型自动推断为 User | null
// 不需要再写 if (!user) return null;
}
四、API 数据契约:Zod 运行时验证
TypeScript 的编译时类型检查在运行时无效——API 返回的数据结构可能与定义不符,导致运行时错误。
4.1 Zod Schema 定义与推断
import { z } from 'zod';
// 定义 Schema
export const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
});
// 类型推断(编译时)
export type User = z.infer<typeof UserSchema>;
// 运行时验证
export function parseUser(data: unknown): User {
return UserSchema.parse(data); // 验证失败时抛出 ZodError
}
// 安全解析(不抛出,返回结果对象)
export function safeParseUser(data: unknown): { success: true; data: User } | { success: false; error: z.ZodError } {
const result = UserSchema.safeParse(data);
if (result.success) {
return { success: true, data: result.data };
}
return { success: false, error: result.error };
}
4.2 API 层统一封装
// utils/api.ts
import { z } from 'zod';
interface ApiResponse<T> {
data: T;
status: number;
}
async function apiFetch<T>(
url: string,
schema: z.ZodSchema<T>,
options?: RequestInit
): Promise<ApiResponse<T>> {
const res = await fetch(url, options);
if (!res.ok) {
throw new ApiError(res.status, await res.text());
}
const json = await res.json();
const parsed = schema.safeParse(json);
if (!parsed.success) {
console.error('API response validation failed:', parsed.error.format());
throw new ApiError(500, 'Invalid response format');
}
return { data: parsed.data, status: res.status };
}
// 使用
const { data: user } = await apiFetch('/api/user/123', UserSchema);
// data 类型自动推断为 User
4.3 API 路由 Zod 验证(Next.js App Router)
// app/api/users/route.ts
import { z } from 'zod';
import { NextRequest, NextResponse } from 'next/server';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().optional(),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const result = CreateUserSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ errors: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
// result.data 类型为 { name: string; email: string; age?: number }
const user = await db.user.create({ data: result.data });
return NextResponse.json(user, { status: 201 });
}
五、严格模式配置:tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/types/*": ["./src/types/*"]
}
},
"include": ["src", "tests"],
"exclude": ["node_modules", "dist"]
}
关键严格选项说明:
| 选项 | 作用 | 推荐值 |
|---|---|---|
strict: true | 开启所有严格类型检查 | ✅ 必须 |
noUnusedLocals | 未使用的局部变量报错 | ✅ 启用 |
exactOptionalPropertyTypes | undefined 不能赋值给可选属性 | ✅ 启用 |
noUncheckedIndexedAccess | 索引访问返回 T | undefined | ✅ 启用 |
noImplicitReturns | 函数没有默认返回值时报错 | ✅ 启用 |
六、常见 TypeScript 错误与修复
6.1 “无法赋值给 ‘RefObject’,因为类型 ‘MutableRefObject<…>’ 不兼容”
// ❌ 错误
const ref = useRef<HTMLInputElement>();
// 类型是 MutableRefObject<HTMLInputElement | undefined>
// 传给需要 RefObject<HTMLInputElement | null> 的组件时不兼容
// ✅ 修正
const ref = useRef<HTMLInputElement>(null);
// 类型是 RefObject<HTMLInputElement | null>
6.2 “对象可能为 ’null’"(严格空检查)
// ❌ 错误:在 !strictNullChecks 下不报错,打开严格模式后暴露
useEffect(() => {
inputRef.current.focus(); // ❌ Object is possibly 'null'
}, []);
// ✅ 修正
useEffect(() => {
inputRef.current?.focus(); // 可选链
}, []);
// 或 if 守卫
if (inputRef.current) {
inputRef.current.focus();
}
6.3 “类型 string | undefined 不能赋值给类型 string”
// ❌ 错误
interface User { name?: string; }
function greet(user: User): string {
return `Hello, ${user.name.toUpperCase()}`; // ❌ name 可能 undefined
}
// ✅ 修正
greet(user: User): string {
return `Hello, ${user.name?.toUpperCase() ?? 'Guest'}`;
}
// 或用非空断言(确信有值时)
return `Hello, ${user.name!.toUpperCase()}`;
6.4 “Event handler 类型不匹配”
// ❌ 错误
function handleClick(event: Event) { // 错误的类型
console.log(event.target.value);
}
<button onClick={handleClick} /> // ❌ 类型不兼容
// ✅ 修正
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log((event.target as HTMLButtonElement).textContent);
}
// 或用 React 提供的 Handler 类型
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
console.log(event.currentTarget.textContent);
};
七、类型速查表
| 场景 | 类型 |
|---|---|
| 组件返回类型 | React.ReactNode(最通用)/ JSX.Element |
| Props children | React.ReactNode |
| Ref 引用 | React.RefObject<T> / React.RefCallback<T> |
| 事件处理器 | React.MouseEventHandler<T> / React.ChangeEventHandler<T> |
| 样式属性 | React.CSSProperties |
| 组件类 | React.ComponentType<Props> |
| 上下文默认值 | MyContextType | null(配合自定义 hook 检查) |
| 函数组件(forwardRef) | React.ForwardRefRenderFunction<E, Props> |
| HTML 属性继承 | React.HTMLAttributes<T> / React.InputHTMLAttributes<HTMLInputElement> |
常见问题(FAQ)
React.FC 为什么不推荐?
React.FC 在 React 18 之前默认包含 children、不自动推断 defaultProps,且无法正确支持泛型组件。推荐直接声明组件函数,让 TypeScript 推断返回类型。
Props 用 interface 还是 type?
推荐 interface:可自动合并声明(Declaration Merging)、扩展用 extends 更直观。type 更适合联合类型和条件类型。对于组件 Props 优先 interface。
第三方库没有类型定义怎么办?
- 搜索
@types/库名(DefinitelyTyped) - 自建类型声明:
declare module '未定义库' { export function foo(): string; } - 如果库是纯 JS 且没类型,考虑类型友好的替代品
泛型组件怎么限制类型参数?
// ❌ 太宽泛
function List<T>(items: T[]) { }
// ✅ 限制为可比较类型
function List<T extends { id: string }>(items: T[]) {
return items.map(item => <li key={item.id}>{item}</li>);
}
相关阅读
- React 详解 — Hooks、Context、受控组件的 TypeScript 模式
- React Hooks 完全指南 — useReducer、useMemo 的泛型用法
- React 状态管理指南 — Zustand/Jotai/RTK 的类型实践
- React 测试指南 — Vitest + RTL 的 TypeScript 测试写法
- React Server Components 深度解析 — 服务端组件的类型安全
- Zod 官方文档
- TypeScript 官方文档
- React TypeScript Cheatsheet
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「frontend」更多文章
Vue 性能优化指南:虚拟列表、懒加载、渲染优化与 Core Web Vitals
Vue 3 应用性能优化完整策略:虚拟滚动(vue-virtual-scroller)、组件懒加载与异步组件、KeepAlive 缓存、Suspense 异步优化、v-memo 渲染记忆化、响应式性能(shallowRef/toRaw)、Bundle 分析与代码分割、渲染函数优化、以及 Core Web Vitals(LCP/INP/CLS)调优。
Nuxt.js 完全指南:Vue 全栈框架的 SSR、SSG、API 路由与部署实践
Nuxt.js 3 深度实践:文件系统路由、SSR/SSG/ISR 渲染模式、API 路由(Server Routes)、useFetch/useAsyncData 数据获取、中间件与插件、SEO 与 Meta 管理、Nitro 服务端引擎、自动导入、部署到 Vercel/Netlify/Node.js。
Vue 测试深度指南:Vitest + Vue Test Utils + Playwright E2E 与 CI 集成
Vue 3 应用从单元测试到 E2E 的完整测试策略:Vitest + Vue Test Utils 组件测试(mount/emits/slots/async)、Pinia Store Mocking、MSW API 拦截、Playwright 端到端测试、Cypress 组件测试、覆盖率标准与 GitHub Actions CI 集成。