R46第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

类型安全的组件设计

类型安全的组件设计 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完你能用 TypeScript 的属性联合与泛型,把「组件支持什么、不支持什么」写进类型:非法的参数组合在编译期报错,列表组件的回调参数类型自动跟随数据类型;理解「类型即文档、报错即设计反馈」,同时知道类型管不到的运行时边界。

问题场景

按钮组件第一版用 JS 写:variant 接受任意字符串,手滑拼错的 "bluе"(末尾混进西里尔字母)混进来毫无反应;提示组件 Toast 有四种形态,loading 态不该有关闭按钮,但调用方随手传了也没人拦;列表组件 List 的 onSelect 回调参数永远是 any,字段名全靠猜。这些都不是运行时 bug,而是契约没有写进类型。

核心解释

两类武器:字面量联合表达封闭集合,可辨识联合表达互斥组合。

type ButtonVariant = "primary" | "ghost" | "danger";
// variant="blue" → 编译期直接报错,拼错活不过 tsc

type ToastProps =
  | { kind: "info"; text: string }
  | { kind: "loading"; text: string } // 没有 closable 字段
  | { kind: "error"; text: string; retry: () => void };

// <Toast kind="loading" text="加载中" closable /> → 报错:loading 态不存在 closable
// <Toast kind="error" text="失败" /> → 报错:缺少必填的 retry

可辨识联合的关键是「判别字段」(kind):TypeScript 按判别值收窄类型——组件内部 switch (props.kind) 时每个分支拿到精确的属性集;外部调用时非法组合直接编译失败。「支持的组合」与「能通过编译的调用」从此是同一个集合,这就是类型作为设计反馈的含义。

泛型解决「类型跟随数据」:

type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  onSelect?: (item: T) => void;
};
function List<T>({ items, renderItem, onSelect }: ListProps<T>) { /* … */ }
// <List items={courses} onSelect={(c) => ...} /> → c 自动是 Course 类型

使用边界也要说清:类型只管编译期,拦不住运行时的脏数据——接口返回的 variant 仍可能非法,入口处仍要校验;类型也别设计得过深,三层以上的条件类型让同事读不懂时,类型本身就成了负债。验证方式:在 TS 文件里故意写出三种非法调用,确认编译器逐个报错,再改对。

示例拆解

正文用 TS 展示契约;实验区用可运行的等价 JS 演示运行时行为:

  • Toast 按 kind 渲染四种形态(info / loading / error / success);JS 版在运行时检查「TS 里本该编译期报错」的调用:未知 kind 直接抛错,loading 态传 closable、error 态缺 retry 输出警告;
  • 泛型 List 在 JS 里退化为普通组件,配合注释标注类型;点击条目,回调直接拿到课程对象;
  • 控制台输出三条「非法用法演示」日志,逐条对应 TS 里会报错的写法;
  • 验证:打开控制台读三条日志,把每条对应回正文中会编译失败的那一行代码。

动手任务

打开本课实验区:

  1. 给 Toast 增加第四种 kind:"success"(带 closable),先在正文风格里写出 TS 类型,再补 JS 渲染分支。
  2. 把 List 的 onSelect 从「传对象」改成「传索引」,评估两种回调契约哪种更类型安全,说出理由。
  3. 给 Toast 的 JS 版补上与 TS 对应的运行时校验(未知 kind 抛错、缺 retry 警告),体会「类型前移到运行时」的差别。

常见错误

  • 什么都用 string:variant、size、kind 这类封闭集合必须用字面量联合;string 等于放弃检查。
  • 可选项当万能补丁:所有 props 都加 ? 之后,非法组合全都不报错了;互斥关系用可辨识联合表达,不用可选堆叠。
  • any 逃逸:回调参数标 any 后整条链路失守;先让泛型推断,推断不动再手写。
  • 运行时数据直接信任类型:接口返回值需要运行时校验;类型断言(as)只是给编译器看的声明,不是防护网。

检查与复盘

自查清单:

  • 能用可辨识联合表达一组互斥的 props 组合,说清判别字段的作用
  • 泛型组件的回调参数类型自动跟随数据类型,不需要手写
  • 记得类型的边界:编译期拦截加入口运行时校验,双保险

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读代码(TypeScript + React 19)。使用方式为 <Select options={users} value={selectedUser} onChange={setSelectedUser} />,其中 users: User[],selectedUser: User | null。T 被推断成什么?如果给 value 传一个不兼容的对象字面量会怎样?

type SelectProps<T> = {
  options: T[];
  value: T | null;
  onChange: (next: T | null) => void;
};

function Select<T>({ options, value, onChange }: SelectProps<T>) {
  // 渲染下拉,选项显示 String(option)
  return null;
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...