工程实践:类型复用与编译性能

本节目标

复用类型最常见两招:

// 运行环境: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 秒过、线上少崩的代码,才是赢。