配 tsconfig 最低要求
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"jsx": "preserve"
}
}
strict: true 是底线。noUncheckedIndexedAccess 让 arr[i] 返回 T | undefined——避免运行时 cannot read property of undefined。
函数组件的类型
type Props = {
title: string;
onClick?: () => void;
children?: React.ReactNode;
};
function Card({ title, onClick, children }: Props) {
return <div onClick={onClick}>{title}{children}</div>;
}
不要用 React.FC(2022 后官方不推荐):
- 隐式加
children(即使你不用) - 不支持泛型
- 显式写 Props 类型更直接
children 的类型
React.ReactNode // 最常用,啥都能放(字符串、元素、null、数组)
React.ReactElement // 只能是单个 React Element
JSX.Element // 单个 JSX 表达式的结果
90% 时间用 React.ReactNode。
事件类型
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... };
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); };
const onKey = (e: React.KeyboardEvent<HTMLInputElement>) => { ... };
不知道哪个?IDE 悬停 onChange 看 prop 类型——它一定写着 ChangeEventHandler<HTMLXxxElement>。
ref 的类型
const inputRef = useRef<HTMLInputElement>(null);
<input ref={inputRef} />
inputRef.current?.focus(); // optional chaining 因为初始 null
类型必须匹配 DOM 元素:HTMLInputElement / HTMLDivElement / HTMLButtonElement……
useState 的类型推断
const [n, setN] = useState(0); // 推断为 number
const [user, setUser] = useState<User | null>(null); // 需显式给
const [arr, setArr] = useState<string[]>([]); // 显式给避免 never[]
陷阱:useState([]) 推断为 never[],push 任何东西都报错。一定显式 useState<X[]>([])。
props 中的事件回调
type Props = {
onSelect: (id: string) => void; // 推荐
onClick: React.MouseEventHandler; // 也行
onChange: React.ChangeEventHandler<HTMLInputElement>;
};
第一种最常用、最清楚。
泛型组件
type ListProps<T> = {
items: T[];
render: (item: T) => React.ReactNode;
};
function List<T>({ items, render }: ListProps<T>) {
return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}
<List items={users} render={(u) => u.name} />
T 由调用处自动推断。
受控 input 的两种风格
// 风格 1:每个 input 单独类型
<input value={text} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setText(e.target.value)} />
// 风格 2:依赖 React 自动推断
<input value={text} onChange={(e) => setText(e.target.value)} /> // ✅ 通常够了
通常风格 2 就行——IDE 会自动推。
Discriminated Union(鉴别联合)
特别适合 React props:
type Props =
| { mode: 'view'; data: User }
| { mode: 'edit'; data: User; onSave: (u: User) => void }
| { mode: 'create'; onSave: (u: User) => void };
function UserPanel(props: Props) {
if (props.mode === 'create') {
// 这里 TS 知道没有 data
return <form onSubmit={() => props.onSave(...)} />;
}
}
比 props.data? 到处判空清晰太多。
Server Component vs Client 的类型
Server Component 默认是 async:
// Next.js 15+:params 和 searchParams 都是 Promise
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const { id } = await params;
const sp = await searchParams;
const user = await db.user.findUnique({ where: { id } });
return <UserCard user={user} />;
}
App Router 自动注入 params / searchParams。Next 14 及以前是同步对象;15+ 改成 Promise。
Client Component 不能 async——返回 JSX,类型同函数组件。
工具类型
ComponentProps<typeof Button> // 拿 Button 的 props 类型
ComponentProps<'input'> // 拿 <input> 的所有原生 props
Omit<Props, 'onClick'>
Pick<Props, 'title' | 'desc'>
Required<Partial<X>>
继承原生元素 props:
type ButtonProps = React.ComponentProps<'button'> & {
variant: 'primary' | 'ghost';
};
这样 Button 自动支持所有原生 button 属性(onClick、disabled、type...)+ 你自定义的。
常见错误
- 导出类型时少用
default:export type Props = ...比 default 好——import { Props }更清晰 as类型断言绕过检查——只在你确信比 TS 更知情时用,否则是埋雷any是放弃治疗——unknown一样能跳过类型检查但强制你后续 narrow- enum:默认不要用
enum,用 union literal type:
// ❌
enum Status { Loading, Done, Error }
// ✅
type Status = 'loading' | 'done' | 'error';
→ 下一篇 测试