工程实践:类型复用与编译性能
本节目标
- 掌握「跨文件/跨包复用类型」的几种姿势
- 知道「类型太多导致 tsc 慢」时的应对
复用类型最常见两招:
// 运行环境:Node.js 16+ + typescript;类型检查 npx tsc --noEmit ts-l20.ts
// 1) 导出 interface/type,别处 import
// types.ts: export interface User { id: number; name: string }
// import type { User } from './types' // 用 import type 只引类型,不引运行时值
// 2) 用 utility types 组合,不重复定义
import type { User } from './types'
type UserPreview = Pick<User, 'id' | 'name'> // 只取部分字段
type NewUser = Omit<User, 'id'> // 去掉 id(新建时还没有)
type UserMap = Record<string, User> // 字符串到 User 的字典编译性能:类型推导(尤其深层递归类型)会拖慢 tsc。
// 性能建议(类型检查慢时):
// - 用 tsconfig "include" 精确限定范围,别让 tsc 扫全仓
// - CI 用 "tsc --incremental" 增量编译
// - 超深递归类型(>5 层)考虑用运行时实现代替类型推导
// - 大型项目用 "project references"(tsconfig references)拆分名词解释
import type:「只导入类型、不导入值」的导入语法。它告诉 TS「这个东西运行时不存在,只是类型」。好处:打包时 tree-shaking 更干净、避免「为了类型而误引了运行时代码」导致的循环依赖。是类型复用的最佳实践。
课后练习
练习 1:Pick<User, 'id' | 'name'> 和手写 { id: number; name: string } 哪个更好?为什么?
答案:用
Pick更好。它「派生」自User,User字段改了UserPreview自动跟着变,避免两处定义漂移。手写副本日后极易忘记同步。
练习 2:为什么建议用 import type 而不是普通 import 引类型?
答案:
import type明确「这是类型、无运行时值」,打包器可安全丢弃、避免误打包;还能防止「类型循环引用」变成「运行时循环依赖」。语义也更清晰。
总结
到这里 TS 旅程告一段落,我的整体观点是:TS 的终极价值是「让类型的真相只有一个来源」。Pick/Omit/Record 这类工具类型,本质是「从单一 User 派生出所有变体」,改一处全局生效——这比复制粘贴十份类型定义健壮十倍。工程层面,把公共类型抽成共享包、用 import type 引入,是大型项目不失控的底线。最后关于性能:类型体操很酷,但「编译要 3 分钟」的代价是真金白银,深层递归类型该收手时就收手,用运行时逻辑补位。TS 是工具,不是炫技场——写出团队敢改、tsc 秒过、线上少崩的代码,才是赢。