从类型标注到类型守卫,构建类型安全的 React 应用
目录
- 为什么 TypeScript 是 React 的最佳拍档
- Props 的类型标注:从简单到精确
- State 的类型推导:不要什么都写类型
- Discriminated Unions:复杂状态建模
- 泛型组件:构建可复用的类型安全组件
- 自定义 Hooks 的类型安全
- Event Handler 的类型处理
- TanStack Query:类型安全的数据获取
- 实战:构建一个类型安全的账户仪表盘
- 常见陷阱与最佳实践
1. 为什么 TypeScript 是 React 的最佳拍档
React 的组件本质上是数据到 UI 的映射函数。这种函数式的本质天然适配 TypeScript 的类型系统——输入(Props/State)有类型,输出(JSX)就能被类型检查覆盖。
在实际开发中,TypeScript 给我带来最直接的几个好处:
- 重构时的心安:改一个字段名,TypeScript 编译器告诉你所有受影响的地方
- IDE 的精准提示:处理复杂的嵌套数据(金融数据模型尤其常见),不用在代码和文档间来回切换
- API 契约的显式化:
interface就是组件与外部世界的”合同”,接口变更一目了然 - 运行时错误减少:Props 类型校验前移到编译期,减少线上
Cannot read property of undefined
2. Props 的类型标注:从简单到精确
2.1 基础 Props 标注
// ❌ 不好的方式:使用 any,失去了类型检查的意义
interface Props {
title: any;
onClick: any;
}
// ✅ 好的方式:精确标注每个字段
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
2.2 Props 中使用其他组件作为 children
金融系统常见的需求:将自定义布局组件作为 children 传入:
// 精确描述 children 的类型
interface CardProps {
title: string;
children: React.ReactNode; // 接受任何 React 可渲染内容
footer?: React.ReactNode; // 可选的页脚
className?: string;
}
function Card({ title, children, footer, className }: CardProps) {
return (
<div className={`card ${className ?? ''}`}>
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// 使用示例
<Card
title="账户余额"
footer={<Button label="查看详情" onClick={() => {}} />}
>
<BalanceDisplay amount={50000} currency="HKD" />
</Card>
2.3 组件作为 Props(Render Props / Slot Pattern)
// 在复杂的金融仪表盘中,经常需要这种模式
// 允许多种渲染策略
interface TableColumn<T> {
key: keyof T;
title: string;
render?: (value: T[keyof T], record: T) => React.ReactNode;
align?: 'left' | 'center' | 'right';
width?: string;
}
interface DataTableProps<T> {
columns: TableColumn<T>[];
data: T[];
loading?: boolean;
emptyText?: string;
onRowClick?: (record: T) => void;
}
function DataTable<T>({
columns,
data,
loading = false,
emptyText = '暂无数据',
onRowClick
}: DataTableProps<T>) {
if (loading) {
return <div className="skeleton-rows">加载中...</div>;
}
return (
<table className="data-table">
<thead>
<tr>
{columns.map(col => (
<th key={String(col.key)} style={{ textAlign: col.align, width: col.width }}>
{col.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr><td colSpan={columns.length}>{emptyText}</td></tr>
) : (
data.map((record, idx) => (
<tr
key={idx}
onClick={() => onRowClick?.(record)}
style={{ cursor: onRowClick ? 'pointer' : 'default' }}
>
{columns.map(col => (
<td key={String(col.key)} style={{ textAlign: col.align }}>
{col.render
? col.render(record[col.key], record)
: String(record[col.key])}
</td>
))}
</tr>
))
)}
</tbody>
</table>
);
}
// 使用泛型 DataTable
interface Transaction {
id: string;
date: string;
description: string;
amount: number;
currency: string;
status: 'COMPLETED' | 'PENDING' | 'FAILED';
}
const columns: TableColumn<Transaction>[] = [
{ key: 'date', title: '日期', width: '120px' },
{ key: 'description', title: '描述' },
{
key: 'amount',
title: '金额',
align: 'right',
render: (value, record) => (
<span style={{ color: record.status === 'FAILED' ? 'red' : 'inherit' }}>
{record.currency} {value.toLocaleString()}
</span>
),
},
{
key: 'status',
title: '状态',
render: (value) => <StatusBadge status={value} />,
},
];
3. State 的类型推导:不要什么都写类型
3.1 useState 的类型推导
TypeScript 能自动推导简单场景的类型,但显式标注有助于文档化和复杂场景的处理:
// ✅ TypeScript 自动推导:简单场景不需要显式标注
const [count, setCount] = useState(0); // count: number
// ✅ 需要显式标注的场景:联合类型
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
// ✅ 显式标注的对象状态
const [user, setUser] = useState<UserProfile | null>(null);
// ✅ 显式标注数组
const [items, setItems] = useState<MenuItem[]>([]);
// ❌ 不要用 any
const [data, setData] = useState<any>({}); // 丧失了类型安全
3.2 useReducer 的类型标注
复杂状态逻辑用 useReducer 配合 TypeScript 效果最好:
// 用 interface 定义状态和 Action 的类型
interface PaymentState {
amount: number;
currency: string;
recipient: string;
status: 'draft' | 'validating' | 'confirming' | 'processing' | 'completed' | 'failed';
error: string | null;
}
type PaymentAction =
| { type: 'SET_AMOUNT'; payload: number }
| { type: 'SET_CURRENCY'; payload: string }
| { type: 'SET_RECIPIENT'; payload: string }
| { type: 'VALIDATE_START' }
| { type: 'VALIDATE_SUCCESS' }
| { type: 'VALIDATE_ERROR'; payload: string }
| { type: 'SUBMIT' }
| { type: 'SUBMIT_SUCCESS' }
| { type: 'SUBMIT_ERROR'; payload: string }
| { type: 'RESET' };
const initialState: PaymentState = {
amount: 0,
currency: 'HKD',
recipient: '',
status: 'draft',
error: null,
};
function paymentReducer(state: PaymentState, action: PaymentAction): PaymentState {
switch (action.type) {
case 'SET_AMOUNT':
return { ...state, amount: action.payload, status: 'draft', error: null };
case 'VALIDATE_START':
return { ...state, status: 'validating' };
case 'VALIDATE_ERROR':
return { ...state, status: 'failed', error: action.payload };
case 'SUBMIT_SUCCESS':
return { ...state, status: 'completed' };
// ... 其他分支
default:
return state;
}
}
4. Discriminated Unions:复杂状态建模
这是 TypeScript + React 中很常用的一种模式。凡是涉及异步数据加载、错误处理和空状态展示的组件,通常都能从中受益。
4.1 问题:嵌套的 if-else 和 null 检查
// ❌ 常见的"防御性编程"代码:层层判断,丑陋且易错
function AccountDashboard({ data, loading, error }: Props) {
if (loading) {
return <Skeleton />;
}
if (error) {
return <ErrorMessage error={error} />;
}
if (data === null) {
return <EmptyState />;
}
// 真正的业务渲染
return (
<div>
<h1>{data.accountName}</h1>
<p>{data.balance}</p>
{/* ... */}
</div>
);
}
4.2 解法:Tagged Union(标签联合)
// ✅ 用 tagged union 替代多个可选字段
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
// 在组件中使用:TypeScript 会在每个分支中自动收窄类型
function AccountDashboard<T>(props: { state: AsyncState<T> }) {
switch (props.state.status) {
case 'idle':
return <Placeholder />;
case 'loading':
return <Skeleton />;
case 'error':
// TypeScript 知道这里 state 是 { status: 'error'; error: Error }
return <ErrorMessage message={props.state.error.message} />;
case 'success':
// TypeScript 知道这里 state 是 { status: 'success'; data: T }
// data 的类型是 T,不需要额外检查
return <Content data={props.state.data} />;
}
}
4.3 实际案例:账户详情组件
// 定义完整的数据类型
interface AccountInfo {
accountNumber: string;
accountName: string;
balance: number;
currency: Currency;
accountType: 'SAVINGS' | 'CHECKING' | 'FIXED_DEPOSIT';
openDate: string;
status: 'ACTIVE' | 'FROZEN' | 'CLOSED';
}
interface TransactionSummary {
recentTransactions: Transaction[];
monthlyInflow: number;
monthlyOutflow: number;
}
// 组合的异步状态
type AccountDashboardState =
| { state: 'idle' }
| { state: 'loading' }
| { state: 'error'; message: string; retry: () => void }
| { state: 'success'; account: AccountInfo; transactions: TransactionSummary };
// 渲染组件
function AccountDashboardView({ state }: { state: AccountDashboardState }) {
switch (state.state) {
case 'idle':
return <EnterAccountNumberPrompt />;
case 'loading':
return <LoadingSkeleton />;
case 'error':
return (
<ErrorCard
message={state.message}
onRetry={state.retry}
/>
);
case 'success':
return (
<div className="dashboard">
<AccountHeader account={state.account} />
<BalanceCard
balance={state.account.balance}
currency={state.account.currency}
/>
<RecentTransactions
transactions={state.transactions.recentTransactions}
/>
<MonthlySummary
inflow={state.transactions.monthlyInflow}
outflow={state.transactions.monthlyOutflow}
/>
</div>
);
}
}
5. 泛型组件:构建可复用的类型安全组件
5.1 基础泛型组件
// 一个支持任意类型列表的选择器组件
interface SelectOption<T> {
value: T;
label: string;
disabled?: boolean;
}
interface GenericSelectProps<T> {
options: SelectOption<T>[];
value: T | null;
onChange: (value: T | null) => void;
placeholder?: string;
labelKey?: keyof T; // 当 T 是对象时,用于显示 label
disabled?: boolean;
}
function GenericSelect<T>({
options,
value,
onChange,
placeholder = '请选择',
disabled = false,
}: GenericSelectProps<T>) {
return (
<select
value={value !== null ? String(value) : ''}
onChange={(e) => {
const selected = options.find(opt => String(opt.value) === e.target.value);
onChange(selected?.value ?? null);
}}
disabled={disabled}
>
<option value="">{placeholder}</option>
{options.map((opt, idx) => (
<option key={idx} value={String(opt.value)} disabled={opt.disabled}>
{opt.label}
</option>
))}
</select>
);
}
// 使用:账户类型选择
interface AccountType {
code: 'SAVINGS' | 'CHECKING' | 'FIXED';
name: string;
minBalance: number;
}
const accountTypes: SelectOption<string>[] = [
{ value: 'SAVINGS', label: '储蓄账户' },
{ value: 'CHECKING', label: '支票账户' },
{ value: 'FIXED', label: '定期存款', disabled: true },
];
<GenericSelect
options={accountTypes}
value={selectedAccountType}
onChange={setSelectedAccountType}
/>
5.2 Forward Ref 与泛型
当需要暴露 DOM 引用时,配合 forwardRef 使用:
// 给需要聚焦的输入组件加 ref 支持
interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, error, ...props }, ref) => {
return (
<div className="input-group">
<label>{label}</label>
<input ref={ref} {...props} className={error ? 'input-error' : ''} />
{error && <span className="error-text">{error}</span>}
</div>
);
}
);
// 显式设置 displayName(方便调试)
TextInput.displayName = 'TextInput';
// 使用 ref
function PaymentForm() {
const accountRef = useRef<HTMLInputElement>(null);
const validateForm = () => {
// TypeScript 知道 ref.current 是 HTMLInputElement
if (!accountRef.current?.value) {
accountRef.current.focus(); // 精准的类型提示和类型安全的调用
}
};
return (
<form>
<TextInput
ref={accountRef}
label="收款账户"
type="text"
required
/>
</form>
);
}
6. 自定义 Hooks 的 Type Safe
自定义 Hooks 是 React 中的”类型安全重灾区”,处理不好会导致组件和 Hook 之间的类型脱节。
6.1 类型安全的 useLocalStorage
// 泛型 + useSyncExternalStore 的组合
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
return [storedValue, setValue] as const; // as const 保证返回的是只读元组
}
// 使用:TypeScript 自动推导类型
const [theme, setTheme] = useLocalStorage('theme', 'light');
// theme: string, setTheme: (value: string | ((val: string) => string)) => void
const [preferences, setPreferences] = useLocalStorage('prefs', {
language: 'zh-CN',
notifications: true,
});
// preferences: { language: string; notifications: boolean }
6.2 类型安全的 useAsync
// 一个封装了 loading/error/success 状态的异步 Hook
type AsyncState<T> =
| { status: 'idle' }
| { status: 'pending' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function useAsync<T>(
asyncFn: () => Promise<T>,
dependencies: unknown[] = []
) {
const [state, setState] = useState<AsyncState<T>>({ status: 'idle' });
useEffect(() => {
let cancelled = false;
setState({ status: 'pending' });
asyncFn()
.then((data) => {
if (!cancelled) {
setState({ status: 'success', data });
}
})
.catch((error) => {
if (!cancelled) {
setState({ status: 'error', error: error instanceof Error ? error : new Error(String(error)) });
}
});
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
return state;
}
// 使用:获取账户信息
interface Account {
id: string;
name: string;
balance: number;
}
const accountState = useAsync<Account[]>(
() => api.getAccounts(),
[accountId]
);
// accountState 的类型被自动推导为 AsyncState<Account[]>
// switch-case 分支中的 data/error 字段自动收窄类型
switch (accountState.status) {
case 'success':
console.log(accountState.data); // Account[]
break;
case 'error':
console.error(accountState.error.message); // Error
break;
}
7. Event Handler 的类型处理
7.1 常用 Event 类型速查
// 表单事件
<form onSubmit={(e: React.FormEvent<HTMLFormElement>) => {}}>
<input
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {}}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {}}
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {}}
/>
<textarea onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {}} />
<select onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {}}>
<option value="hkd">HKD</option>
</select>
</form>
// 鼠标事件
<div onClick={(e: React.MouseEvent<HTMLDivElement>) => {}} />
<div onDoubleClick={(e: React.MouseEvent<HTMLDivElement>) => {}} />
<div onContextMenu={(e: React.MouseEvent<HTMLDivElement>) => {}} />
// 键盘事件
<input
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSubmit();
}
}}
/>
// 拖拽事件
<div
draggable
onDragStart={(e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('text/plain', dragData);
}}
onDrop={(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
}}
/>
7.2 通用的 Form Handler
// 用泛型函数处理表单字段更新
type FormState<T> = Partial<Record<keyof T, string>>;
function useForm<T extends Record<string, string>>(initialValues: T) {
const [values, setValues] = useState<FormState<T>>(initialValues);
const handleChange = useCallback(
(field: keyof T) => (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setValues(prev => ({ ...prev, [field]: e.target.value }));
},
[]
);
const reset = useCallback(() => {
setValues(initialValues);
}, [initialValues]);
return { values, handleChange, setValues, reset };
}
// 使用:账户开户表单
interface AccountFormData {
accountName: string;
idNumber: string;
email: string;
phone: string;
currency: string;
}
const { values, handleChange, reset } = useForm<AccountFormData>({
accountName: '',
idNumber: '',
email: '',
phone: '',
currency: 'HKD',
});
// JSX 中的使用
<input
name="accountName"
value={values.accountName ?? ''}
onChange={handleChange('accountName')}
/>
8. TanStack Query:类型安全的数据获取
TanStack Query(原 React Query)是 React 生态里很成熟的数据获取库。配合 TypeScript 使用,类型约束会清晰很多。
8.1 类型安全的 queryFn
// 定义 API 响应类型
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface Account {
id: string;
accountNumber: string;
balance: number;
currency: string;
}
// 类型通过 generic 传递,queryFn 不需要返回类型注解
function useAccount(accountId: string) {
return useQuery<ApiResponse<Account>, Error>({
queryKey: ['account', accountId],
queryFn: async () => {
const response = await fetch(`/api/accounts/${accountId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
},
// staleTime: 5分钟,数据在5分钟内不会重新发起请求
staleTime: 5 * 60 * 1000,
// enabled: 只有当 accountId 存在时才发起请求
enabled: !!accountId,
});
}
// 在组件中使用
function AccountCard({ accountId }: { accountId: string }) {
const { data, isLoading, isError, error, refetch } = useAccount(accountId);
if (isLoading) return <Skeleton />;
if (isError) {
return (
<div>
<p>加载失败: {error.message}</p>
<Button label="重试" onClick={() => refetch()} />
</div>
);
}
// data.data 的类型是 Account(嵌套在 ApiResponse 中)
const account = data!.data;
return (
<Card>
<h3>{account.accountNumber}</h3>
<p>{account.currency} {account.balance.toLocaleString()}</p>
</Card>
);
}
8.2 乐观更新与 TypeScript
// TanStack Query 的乐观更新场景
function useUpdateAccount() {
const queryClient = useQueryClient();
return useMutation<
ApiResponse<Account>,
Error,
{ accountId: string; updates: Partial<Account> }
>({
mutationFn: async ({ accountId, updates }) => {
const response = await fetch(`/api/accounts/${accountId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates),
});
if (!response.ok) throw new Error('Update failed');
return response.json();
},
onMutate: async ({ accountId, updates }) => {
// 取消任何正在进行的同名 query
await queryClient.cancelQueries({ queryKey: ['account', accountId] });
// 保存旧值,用于回滚
const previousAccount = queryClient.getQueryData<ApiResponse<Account>>(['account', accountId]);
// 乐观更新:立即显示新值
queryClient.setQueryData<ApiResponse<Account>>(['account', accountId], (old) => {
if (!old) return old;
return {
...old,
data: { ...old.data, ...updates },
};
});
// 返回旧值以供 rollback 使用
return { previousAccount };
},
onError: (err, { accountId }, context) => {
// 出错时回滚
if (context?.previousAccount) {
queryClient.setQueryData(['account', accountId], context.previousAccount);
}
},
onSettled: ({ accountId }) => {
// 成功后重新验证,确保数据与服务器一致
queryClient.invalidateQueries({ queryKey: ['account', accountId] });
},
});
}
9. 实战:构建一个类型安全的账户仪表盘
综合运用以上所有技术:
// === types/financial.ts ===
export type Currency = 'HKD' | 'USD' | 'GBP' | 'EUR' | 'CNY';
export interface Money {
amount: number;
currency: Currency;
}
export interface Transaction {
id: string;
description: string;
counterparty: string;
amount: Money;
timestamp: string;
category: string;
status: 'COMPLETED' | 'PENDING' | 'FAILED';
}
export interface AccountSummary {
accountId: string;
accountNumber: string;
accountName: string;
balance: Money;
availableBalance: Money;
recentTransactions: Transaction[];
monthlyStats: {
inflow: Money;
outflow: Money;
transactionCount: number;
};
}
// === hooks/useAccountSummary.ts ===
import { useQuery } from '@tanstack/react-query';
import type { AccountSummary } from '../types/financial';
interface UseAccountSummaryOptions {
accountId: string | null;
timeRange?: '7d' | '30d' | '90d';
}
export function useAccountSummary({ accountId, timeRange = '30d' }: UseAccountSummaryOptions) {
return useQuery({
queryKey: ['account-summary', accountId, timeRange],
queryFn: async (): Promise<AccountSummary> => {
const params = new URLSearchParams({ range: timeRange });
const response = await fetch(`/api/accounts/${accountId}/summary?${params}`);
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`);
const result = await response.json();
if (result.code !== 0) throw new Error(result.message);
return result.data;
},
enabled: !!accountId,
staleTime: 60 * 1000, // 1分钟
});
}
// === components/AccountDashboard.tsx ===
import { useAccountSummary } from '../hooks/useAccountSummary';
type DashboardState =
| { status: 'no-account-selected' }
| { status: 'loading' }
| { status: 'error'; message: string; onRetry: () => void }
| { status: 'success'; summary: AccountSummary };
function AccountDashboard({ accountId }: { accountId: string | null }) {
const { data, isLoading, isError, error, refetch, isFetching } = useAccountSummary({
accountId,
});
const state: DashboardState = (() => {
if (!accountId) return { status: 'no-account-selected' };
if (isLoading) return { status: 'loading' };
if (isError) return { status: 'error', message: error.message, onRetry: refetch };
return { status: 'success', summary: data! };
})();
return (
<div className="account-dashboard">
{/* 全局 loading 指示器(不是首次加载的 loading) */}
{isFetching && state.status !== 'loading' && <GlobalLoadingBar />}
{(() => {
switch (state.status) {
case 'no-account-selected':
return <EmptyState message="请选择一个账户" />;
case 'loading':
return <DashboardSkeleton />;
case 'error':
return <ErrorCard message={state.message} onRetry={state.onRetry} />;
case 'success':
return <DashboardContent summary={state.summary} />;
}
})()}
</div>
);
}
10. 常见陷阱与最佳实践
10.1 不要用 as 做类型断言
// ❌ as 断言是"强制说服" TypeScript,掩盖了问题
const data = response.data as Account;
// ✅ 用类型守卫或条件检查
const data: Account | null = response.data;
if (!data) return null; // 让逻辑分支来处理 null 的情况
10.2 Props 不要过度设计
// ❌ 过度设计:很多接口字段永远不会被用到
interface CardProps {
title: string;
subtitle?: string;
description?: string;
footer?: React.ReactNode;
onClick?: () => void;
onHover?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: void;
className?: string;
style?: React.CSSProperties;
id?: string;
dataTestId?: string;
}
// ✅ 够用就好:只定义实际使用的字段
interface CardProps {
title: string;
children?: React.ReactNode;
footer?: React.ReactNode;
onClick?: () => void;
className?: string;
}
10.3 用 zod 做运行时验证(API 响应校验)
前端 TypeScript 只做编译期检查,API 返回的数据是”不可信”的:
import { z } from 'zod';
const MoneySchema = z.object({
amount: z.number(),
currency: z.enum(['HKD', 'USD', 'GBP', 'EUR', 'CNY']),
});
const AccountSchema = z.object({
accountId: z.string().uuid(),
accountNumber: z.string().length(12), // 香港银行账户12位
balance: MoneySchema,
status: z.enum(['ACTIVE', 'FROZEN', 'CLOSED']),
});
// API 响应验证
const result = AccountSchema.safeParse(apiResponse.data);
if (!result.success) {
// 有数据不符合预期结构,记录日志并降级处理
logger.error('API response validation failed', result.error.flatten());
throw new Error('Invalid data from API');
}
const account = result.data; // TypeScript 知道这里 account 是 Account 类型
10.4 TypeScript 配置建议
// tsconfig.json — 生产环境推荐配置
{
"compilerOptions": {
"strict": true, // 开启所有严格类型检查
"noImplicitAny": true, // 禁止隐式 any
"strictNullChecks": true, // 严格 null 检查
"noUnusedLocals": true, // 不允许未使用的局部变量
"noUnusedParameters": true, // 不允许未使用的参数
"exactOptionalPropertyTypes": true, // 可选属性的类型更精确
"jsx": "react-jsx"
}
}
结语
React + TypeScript 的组合,本质上是让类型系统在运行时之前就发现错误。在金融系统的开发中,这种”早发现、早修复”的能力尤为重要——每一笔资金交易都不能容忍运行时类型错误。
核心原则就三条:
- 精确优于宽泛:用
string而不用any,用union type而不用object - 让 TypeScript 收窄类型:多用
switch/if的分支收窄,少用as强制断言 - 类型即文档:好的类型标注本身就是最好的代码注释
Bobot | 2026-03-19