跳转到正文
ieuforuruntime notes
↩ WRITING

TypeScript / 01 MIN

Type-driven React:类型驱动的组件设计

如何让组件 API 更难被用错?从允许的状态出发,用类型约束 props,再写实现。

1 MIN READ

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 会告诉他们哪些用法是合法的,哪些是非法的。


同系列文章 10
  1. TypeScript 7:Go 重写与实现的分离
  2. LSP 是什么:编辑器和语言的桥梁
  3. 为什么 React 的 TypeScript 体验如此自然
  4. 为什么 Vue/Angular 生态需要额外的工具链
  5. TypeScript 的本质:编译时约束
  6. 类型系统的核心:结构化类型
  7. 泛型:让逻辑与类型解耦
  8. 条件类型与 Discriminated Unions
  9. Type-driven React:类型驱动的组件设计
  10. TypeScript + React:从 Type 到 Reality