第 19 章:TypeScript + React
学习目标
- 掌握 React 组件的 4 种类型定义
- 熟练使用常见 hooks 的泛型
- 处理事件、ref、children 类型
- 避开 TypeScript 的 5 个常见坑
一、组件 Props 类型
1. interface(推荐)
tsx
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'danger';
children?: React.ReactNode;
}
function Button({ label, onClick, variant = 'primary', children }: ButtonProps) {
return <button onClick={onClick} className={`btn btn-${variant}`}>{children ?? label}</button>;
}2. type(联合/复杂类型)
tsx
type Status = 'loading' | 'success' | 'error';
interface CardProps {
status: Status;
data?: unknown;
}3. 继承 HTML 属性
tsx
import { ButtonHTMLAttributes } from 'react';
interface MyButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary';
}
function MyButton({ variant = 'primary', ...rest }: MyButtonProps) {
return <button className={`btn btn-${variant}`} {...rest} />;
}⚠️ 坑 1:不继承原生属性就拿不到
onClick、disabled等标准 props。
4. 函数组件类型(React.FC)
tsx
const Card: React.FC<{ title: string }> = ({ title, children }) => (
<div>
<h3>{title}</h3>
{children}
</div>
);⚠️ 坑 2:React 18 起
React.FC不再隐式包含children,且会让泛型变复杂。推荐用 interface + 普通函数。
二、children 类型
tsx
interface Props {
children: React.ReactNode; // 最宽,任何可渲染内容
// 或更精确:
children: React.ReactElement; // 必须是 React 元素
// 或:
renderItem: (item: Item) => React.ReactNode; // 渲染函数
}三、useState 类型
tsx
// 自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// 显式类型
const [user, setUser] = useState<User | null>(null);
// 联合类型 + 判别
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
const [state, setState] = useState<AsyncState<User>>({ status: 'idle' });
if (state.status === 'success') {
console.log(state.data); // ✅ 类型已收窄
}四、useRef 类型
tsx
// DOM 引用
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // ⚠️ 必须用 ?.
// 值 ref
const countRef = useRef<number>(0);
countRef.current = 1;五、useReducer 类型
tsx
interface State { count: number; }
type Action =
| { type: 'inc' }
| { type: 'dec' }
| { type: 'set'; value: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'inc': return { count: state.count + 1 };
case 'dec': return { count: state.count - 1 };
case 'set': return { count: action.value };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 });⚠️ 坑 3:
switch必须穷举所有 action,加defaultreturn state 防止漏处理。
六、事件类型
tsx
function Form() {
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.currentTarget);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') console.log('回车');
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onKeyDown={handleKey} />
<button onClick={handleClick}>点</button>
</form>
);
}七、自定义 Hook 类型
tsx
function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key);
return saved ? (JSON.parse(saved) as T) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}⚠️ 坑 4:返回元组时类型用
[T, (v: T) => void],不要写any[]。
八、forwardRef 类型
tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
interface MyInputRef { focus: () => void; clear: () => void; }
interface MyInputProps { defaultValue?: string; }
const MyInput = forwardRef<MyInputRef, MyInputProps>(
({ defaultValue }, ref) => {
const innerRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => innerRef.current?.focus(),
clear: () => { if (innerRef.current) innerRef.current.value = ''; },
}));
return <input ref={innerRef} defaultValue={defaultValue} />;
},
);九、泛型组件
tsx
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyOf: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyOf(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用
<List<User>
items={users}
renderItem={u => <span>{u.name}</span>}
keyOf={u => u.id}
/>十、常用工具类型
tsx
type Partial<T> = { [K in keyof T]?: T[K] }; // 全可选
type Pick<T, K extends keyof T> = { [P in K]: T[P] }; // 选字段
type User = { id: number; name: string; age: number };
type UserPreview = Pick<User, 'id' | 'name'>; // { id, name }
// React 提供的:
type PropsWithChildren<P> = P & { children?: React.ReactNode };⚠️ 坑 5:
Omit<HTMLProps, 'onClick'>这种操作可以"剔除"原生事件,做组件 API 设计。
十一、tsconfig 关键配置
json
{
"compilerOptions": {
"jsx": "react-jsx", // ⚠️ 新 JSX 运行时,不用 import React
"strict": true, // 严格模式
"noUncheckedIndexedAccess": true,
"skipLibCheck": true
}
}十二、本章小结
| 要点 | 关键 |
|---|---|
| Props | interface + 普通函数组件 |
| 继承原生属性 | extends HTMLAttributes<T> |
| useState | 显式标注联合/判别类型 |
| useRef | useRef<T>(null) + ?. |
| 事件类型 | React.XxxEvent<T> |
| 泛型组件 | <T> 形参 + keyof 约束 |
| 不要 | React.FC(隐式 children 没了) |
动手练习
- Button 类型:支持
variant、size、disabled,继承原生 button 属性 - 泛型 List:接受任意类型数组 + render 函数
- AsyncState:用判别联合类型实现 loading/data/error 三态
- 找坑:以下代码 TS 报错为什么?tsx
const ref = useRef<HTMLInputElement>(null); ref.current.focus();
下一章:第 20 章:Next.js 入门 →