TypeScript 在 React 中的最高价值不是"给 props 加类型"。
而是:
类型先行:先定义组件允许表达什么,再写实现。
类型先行
// ❌ 先写实现,再补类型
function Modal(props) {
return (
<div className="modal">
<div className="modal-header">{props.title}</div>
<div className="modal-body">{props.children}</div>
<div className="modal-footer">
<button onClick={props.onClose}>关闭</button>
</div>
</div>
)
}
// 然后再补类型,但已经晚了——API 设计已经定型
// ✅ 先定义类型,再写实现
interface ModalProps {
title: string
children: React.ReactNode
onClose: () => void
size?: 'sm' | 'md' | 'lg'
closeOnOverlay?: boolean
}
function Modal({ title, children, onClose, size = 'md', closeOnOverlay = true }: ModalProps) {
// ...
}
类型先行的好处:
- 在写实现之前,就知道组件的 API 长什么样
- 类型约束了组件可以做什么、不可以做什么
- 使用者在调用时就能看到所有选项
用类型设计不容易被错误使用的 API
原则 1:用 union 类型限制选项
// ❌ 字符串,容易拼错
interface ButtonProps {
variant: string
}
// ✅ union 类型,拼错就报错
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger'
}
原则 2:用 discriminated union 处理互斥状态
// ❌ 容易同时传 icon 和 loading,语义冲突
interface ButtonProps {
icon?: React.ReactNode
loading?: boolean
}
// ✅ discriminated union,互斥
type ButtonProps = { loading: true; icon?: never } | { loading?: false; icon?: React.ReactNode }
原则 3:用泛型让类型贯穿输入输出
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
keyExtractor: (item: T) => string
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>{renderItem(item, index)}</li>
))}
</ul>
)
}
使用时:
interface User {
id: string
name: string
}
;<List
items={users}
renderItem={(user) => <span>{user.name}</span>} // user 的类型是 User
keyExtractor={(user) => user.id}
/>
renderItem 和 keyExtractor 的参数类型自动推导为 User。
设计流程
类型驱动的组件设计流程:
需求
↓
状态空间(这个组件可能处于哪些状态?)
↓
类型(用 discriminated union 描述状态)
↓
Props API(组件接受什么参数?)
↓
组件实现
核心洞察
不是给已经写好的 JavaScript 补类型,而是让类型参与设计。
好的 API 通过类型把错误使用变得困难。
类型约束了组件可以做什么、不可以做什么。使用者在调用时,TypeScript 会告诉他们哪些用法是合法的,哪些是非法的。