
在 React 项目中设置 TypeScript
# 使用 Vite 创建新项目(2026 年推荐)
npm create vite@latest my-app -- --template react-ts
# 为现有 React 项目添加 TypeScript
npm install -D typescript @types/react @types/react-dom
npx tsc --init
类型化组件 Props
Interface 与 type alias
// Interface — 推荐用于 props(可扩展,错误信息更清晰)
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
children?: React.ReactNode;
}
// Type alias — 适用于联合类型和计算类型
type ButtonVariant = 'primary' | 'secondary' | 'danger';
函数组件
// 现代方式 — 不要使用 React.FC(已在 React 18 中弃用)
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return (
<button
className={clsx('btn', `btn-${variant}`)}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
// 箭头函数风格
const Button = ({ label, onClick }: ButtonProps) => (
<button onClick={onClick}>{label}</button>
);
// 导出类型
export type { ButtonProps };
export { Button };
扩展 HTML 元素 props
// 扩展原生 HTML props — 用户可以传递任何有效的 button 属性
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
variant?: 'primary' | 'secondary';
isLoading?: boolean;
}
function Button({ label, variant = 'primary', isLoading, ...rest }: ButtonProps) {
return (
<button
className={clsx('btn', `btn-${variant}`, { loading: isLoading })}
disabled={isLoading || rest.disabled}
{...rest} // 展开 onClick, type, form 等
>
{isLoading ? <Spinner /> : label}
</button>
);
}
Children 类型
interface CardProps {
children: React.ReactNode; // 最宽松:React 可渲染的任何内容
header?: React.ReactElement; // 必须是 React 元素(不是字符串/数字)
footer?: string | React.ReactElement; // 字符串或元素
}
// 对于只接受特定子组件的组件
interface ListProps {
children: React.ReactElement<ListItemProps> | React.ReactElement<ListItemProps>[];
}
组件状态的可辨识联合
// ❌ 不清晰:哪些组合是有效的?
interface DataViewProps {
status: 'idle' | 'loading' | 'success' | 'error';
data?: User[];
error?: Error;
}
// ✅ 清晰:每个状态都是明确的
type DataViewProps =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: Error };
function DataView(props: DataViewProps) {
if (props.status === 'idle') return <EmptyState />;
if (props.status === 'loading') return <Spinner />;
if (props.status === 'error') return <ErrorMessage error={props.error} />;
// TypeScript 知道此处 status 为 'success' 且 data: User[] 存在
return <UserList users={props.data} />;
}
类型化 Hooks
useState
// TypeScript 从初始值推断类型
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// 初始值为 null/undefined 时显式指定类型
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<Item[]>([]);
// 复杂状态
interface FormState {
name: string;
email: string;
errors: Record<string, string>;
}
const [form, setForm] = useState<FormState>({
name: '',
email: '',
errors: {},
});
useRef
// DOM 元素 ref
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// 可变值 ref(无 null)
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const countRef = useRef<number>(0);
// 使用
useEffect(() => {
inputRef.current?.focus(); // 可选链,因为 DOM ref 初始为 null
}, []);
useReducer
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' }
| { type: 'SET'; payload: number }
| { type: 'RESET' };
interface CounterState {
count: number;
history: number[];
}
function reducer(state: CounterState, action: Action): CounterState {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1, history: [...state.history, state.count] };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
case 'SET':
return { ...state, count: action.payload }; // TypeScript 知道 payload 存在
case 'RESET':
return { count: 0, history: [] };
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, history: [] });
return (
<div>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'SET', payload: 10 })}>Set 10</button>
</div>
);
}
自定义 Hooks
// 返回元组 — 使用 'as const' 类型
function useToggle(initial = false): [boolean, () => void] {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
// 返回对象
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
})
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 使用 — TypeScript 推断 User[] 类型
const { data, loading, error } = useFetch<User[]>('/api/users');
泛型组件
// 适用于任何类型的列表组件
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = 'No items' }: ListProps<T>) {
if (items.length === 0) return <div className="empty-state">{emptyMessage}</div>;
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用 TypeScript 推断
interface User {
id: string;
name: string;
email: string;
}
function UserList({ users }: { users: User[] }) {
return (
<List<User>
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name} — {user.email}</span>}
emptyMessage="No users found"
/>
);
}
事件处理器类型
// 常见事件处理器类型
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') handleSubmit(e as any);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKeyDown} />
<button onClick={handleClick}>Submit</button>
</form>
);
}
Context 与 TypeScript
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// 创建带有合理默认值或 undefined 的 context
const ThemeContext = React.createContext<ThemeContextValue | undefined>(undefined);
// 带空值检查的自定义 hook
function useTheme(): ThemeContextValue {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used inside ThemeProvider');
}
return context;
}
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = useCallback(() => {
setTheme(t => (t === 'light' ? 'dark' : 'light'));
}, []);
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
React 中常见的 TypeScript 错误(及修复)
| 错误 | 原因 | 修复 |
|---|---|---|
Type 'string' is not assignable to type 'never' |
可辨识联合不匹配 | 添加适当的类型守卫 |
Object is possibly 'null' |
useRef 或条件值 | 可选链 ?. 或空值检查 |
Property 'X' does not exist on type 'Y' |
使用了错误的接口 | 检查 prop 类型或扩展接口 |
Cannot invoke an object which is possibly 'undefined' |
可选的函数 prop | 使用 onClick?.() 守卫 |
| JSX element type does not have any construct signatures | 将组件作为 prop 传递但类型不正确 | 使用 React.ComponentType<Props> |
快速参考
// Children
children: React.ReactNode // 任何可渲染内容
children: React.ReactElement // 必须是 JSX 元素
children: React.PropsWithChildren<Props> // 向 Props 添加 children
// Refs
ref: React.RefObject<HTMLDivElement> // 只读
ref: React.MutableRefObject<number> // 可变
// Events
onClick: React.MouseEventHandler<HTMLButtonElement>
onChange: React.ChangeEventHandler<HTMLInputElement>
onSubmit: React.FormEventHandler<HTMLFormElement>
// 组件类型
type FC = (props: Props) => React.ReactElement | null
ComponentType<Props> // 类组件或函数组件
ElementType // 字符串标签或组件
总结
2026 年强类型 TypeScript + React 实践:
- 使用 interfaces 定义 props — 更易扩展,错误信息更清晰
- 使用可辨识联合处理组件状态 — 消除不可能的状态
- 在 TypeScript 无法推断时显式类型化 hooks(useState 与 null,useRef)
- 泛型组件用于可复用的列表、表格和表单字段
- 扩展 HTML 属性使自定义组件仍能接受原生 props
- useContext 配合自定义 hooks — 在 hook 中抛出错误,而非每个调用点
- 避免使用
React.FC— 已在 React 18 中弃用,直接类型化 props
→ 在 JSON Viewer 和 URL Parser 中使用真实 TypeScript 工具进行练习。