TypeScript 2.0–2.9 核心特性精读:从严格模式到类型体操的基石
发布时间:2026/10/2 13:32:46 作者:尧图编辑部 阅读量:1,286

文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本文基于前端精读周刊对 TypeScript 2.0–2.9 官方文档的深度解读原文见 前沿技术/58.精读《Typescript2.0 - 2.9》.md从功能角度而非版本角度梳理这段时期最重要的类型系统能力严格模式下的非空断言与条件类型、never/object基础类型、readonly与映射类型修饰、keyof驱动的内置工具类型、泛型默认参数、动态import()类型、字符串枚举、定长元组与in类型收窄。读完本文你将掌握这些特性的实战用法理解它们如何构成今天 TS 类型体操与大型前端工程类型化的基石并能在本仓库的 TS 类型体操 系列中继续深化练习。为什么要按功能、而非按版本来学 TS 2.0–2.9原文档开篇提出了一个非常尖锐的观察许多写了一年以上 TypeScript 的开发者对 TS 的理解和使用水平仍停留在入门阶段。原因在于TS 的知识积累需要刻意练习使用 TypeScript 的时间与对它的了解程度几乎没有关系。因此本文不再机械地罗列 2.0 到 2.9 每个小版本的更新日志而是精选这段时期最重要的功能配合实际案例解读。对于纯内部优化、用户无感的部分例如编译速度提升、诊断信息改进不做罗列——这些优化会在日常使用中自然被感受到。原文档还给出了一条贯穿全文的学习建议由于 TypeScript 在严格模式下的许多表现都与非严格模式不同为了避免不必要的记忆负担建议只记严格模式下的行为。后续所有特性讲解都建立在strict模式开启的语境下。严格模式从防御式检查到类型级非空断言strict 模式下的高频报错场景直接访问一个变量的属性时如果该变量是undefined不但属性访问不到JS 运行时还会抛出异常——这几乎是业务开发中最高频的报错往往是后端数据异常导致的。而 TS 的strict模式会在编译期检查这种情况不允许不安全的代码出现把运行时异常提前到编译期暴露。以一份静态配置文件为例在 2.0 之前代码往往需要写防御式判断const config { port: 8000 }; if (config) { console.log(config.port); }2.0 的非空断言!.TS 2.0 提供了非空断言标志符!.用于明确告知编译器这个值绝不会是null/undefined从而省去防御式判断。比如静态配置文件一定存在那么直接断言即可console.log(config!.port);2.8 的条件类型让断言自动化TS 2.8 引入了条件类型语法type TypeNameT T extends string ? string : other;当T的类型是string时TypeNameT的表达式结果为string。条件类型让类型系统拥有了分支能力进而可以构造一个自动非空断言的类型把代码简化为console.log(config.port);前提是框架先把config指定为一个特殊类型该类型的定义如下export type PowerPartialT { [U in keyof T]?: T[U] extends object ? PowerPartialT[U] : T[U] };也就是说2.8 的条件类型允许类型判断递归进行把对象的所有 key 都递归地包一层可选修饰。原文档指出这一用法灵感来自 egg-ts 的实战总结。仓库佐证条件类型 递归已是类型体操的常规武器从本仓库的 TS 类型体操/244.精读《Get return type, Omit, ReadOnly...》.md 可以看到递归式条件类型正是 TS 2.8 之后处理复杂对象的标配手段例如手写DeepReadonlytype DeepReadonlyT { readonly [K in keyof T]: T[K] extends object ? DeepReadonlyT[K] : T[K] }以及 TS 类型体操/252.精读《Unique, MapTypes, Construct Tuple...》.md 中用infer 递归实现数组去重与类型转换。可以说PowerPartial所演示的条件类型 递归组合是后续类型体操中最常出现的基本功。never 与 object两种全新的基础类型never永远不会发生的返回值当一个函数无法执行完或者理解为中途中断时TS 2.0 认为它的返回类型是never。典型场景是throw Error或while(true)它们都会让函数返回值类型变为neverfunction fail(): never { throw new Error(something failed); } function loop(): never { while (true) {} }理解never需要抓住一个核心概念预定义了类型不代表类型一定如预期。就好比函数运行时可能因为throw Error而中断所以 TS 把null、undefined设定为所有类型的子类型任何有类型的值都有可能为空从 2.0 开始函数的返回值类型又多了一种子类型never。never与null/undefined一样都是子类型——比如类型number自带null与undefined这两个子类型任何有类型的值都有可能是空执行期间可能没有值。never在后来的版本中成为空联合类型与条件类型兜底分支的事实标准。本仓库 TS 类型体操/243.精读《Pick, Awaited, If...》.md 中多处使用never作为分支不命中的兜底例如type FirstT extends any[] T extends [] ? never : T[0]object描述非基础类型而不是描述具体对象结构TS 2.x原文档分别提到 2.2 与 2.3 两个版本号引入了object类型用于描述非基础类型。但许多人总把object与any弄混淆比如下面的代码const persion: object { age: 5 }; console.log(persion.age); // Error: Property age does not exist on type object.问题出在哪里object类型仅表示它是一个对象类型不承认任何具体的 key。将能精确推导的对象类型扩大为整体的、模糊的object类型后TS 自然无法推断这个对象拥有哪些 key。此时闭眼改成any并不解决问题正确的做法是把object删掉交给 TS 自动推导。object的正确用法是作为类型校验的约束比如作为函数参数类型允许任何对象数据传入但不允许3、abc这类非对象类型declare function create(o: object | null): void; create({ prop: 0 }); // 正确 create(null); // 正确 create(42); // 错误 create(string); // 错误 create(false); // 错误 create(undefined); // 错误修饰符体系readonly、/-与 RequiredTS 2.0 支持了readonly修饰符被它修饰的变量无法被修改interface Point { readonly x: number; readonly y: number; }TS 2.8 又增加了-与修饰修饰符——有点像副词作用于形容词。readonly本质就是readonly因此我们也可以用-readonly移除只读特性同理可以通过-?:的方式移除可选标记。基于此可以延伸出一种新类型RequiredT将对象所有可选修饰移除自然就成为了必选类型type RequiredT { [P in keyof T]-?: T[P] };本仓库 TS 类型体操/243.精读《Pick, Awaited, If...》.md 从另一个角度演示了映射修饰的运用在{ [A in keyof B]: B[A] }的每个 key 前加上readonly即可手写MyReadonly把:换成?:即可得到可选版本type MyReadonlyT { readonly [K in keyof T]: T[K] } type OptionalT { [K in keyof T]?: T[K] }可见{ [A in keyof B]: B[A] }给了我们描述每一个 key 属性细节的机会限制发挥的只有想象力。可以定义函数的 this 类型同样是 TS 2.0我们可以定制this的类型。这在 Vue 等框架中尤为有用例如约束组件方法内this的形态function f(this: void) { // make sure this is unusable in this standalone function }this类型是一种假参数它并不会影响函数真正参数的数量与位置只不过定义在参数位置上而且永远会插队在第一个。引用、寻址支持通配符了简单来说模块名可以用*表示任意单词了declare module *!text { const content: string; export default content; }它的类型可以辐射到任意匹配该模式的导入import fileContent from ./xyz.txt!text;这个特性非常强大的一个点是包括tsconfig.json的模块查找也支持通配符。以原文档列举的 umi 框架为例其locale插件安装后即可从umi/locale获取国际化内容import { locale } from umi/locale;实际上插件是通过webpack.alias创建一个文件并将引用指过去的。要为它补上类型支持只需在tsconfig.json配置paths通配符{ compilerOptions: { paths: { umi/*: [umi, somePath] } } }将所有umi/*的类型都指向somePath那么umi/locale就会指向somePath/locale.ts这个文件如果插件自动创建的文件名恰好也叫locale.ts类型就自动对应上了。这是运行时别名 编译期路径映射协同工作的经典案例。skipLibCheck跳过仓库类型报错TS 2.x 支持了许多新的compilerOptions但skipLibCheck在原文档看来太耀眼了需要单独提出。skipLibCheck这个属性不但可以忽略 npm 包不规范带来的报错还能最大限度的保留类型系统可谓一举两得。拿某 UI 库举例某天发布的小版本.d.ts文件出现一个漏洞导致整个项目构建失败——此时你不再需要提 PR 催促作者修复skipLibCheck可以忽略这种报错同时保持类型的自动推导能力。这比declare module ui-lib将类型整体降级为any更强前者是忽略声明文件内部的错误但保留其类型结构后者是彻底放弃对该模块的类型检查。keyof 与映射类型类型操作革命的起点TS 2.1 是针对类型操作革命性的版本。我们通过keyof拿到对象 key 的类型interface Person { name: string; age: number; } type K1 keyof Person; // name | age基于keyof可以增强对象的类型type NewObjTypeT { [P in keyof T]: T[P] };这里有两个原文档特别标注的 TipsTS 2.8可以以表达式作为keyof的参数比如keyof (A B)。TS 2.9keyof可能返回非string类型的值例如number索引因此从一开始就不要认为keyof的返回类型一定是string。NewObjType原封不动地把对象类型重新描述了一遍看上去没什么意义。但实际上它有三个可拓展的位置左边比如加readonly修饰把对象的属性变成只读。中间比如把:改成?:把对象所有属性变成可选。右边比如套一层PromiseT[P]把对象每个 key 的 value 类型整体覆盖。基于映射类型的内置工具类型全家桶基于这些能力TS 拓展出一系列内置工具类型以下类型都内置在lib.d.ts中不需要定义即可直接使用可以认为是 TypeScript 的 utils 工具库工具类型作用版本ReadonlyT把对象 key 全部设置为只读也可结合 2.8 条件类型实现递归只读2.1PartialT把对象的 key 都设置为可选2.1PickT, K从对象类型 T 挑选一些属性 K对象有 10 个 key把 K 设为name | age即生成仅含这两个 key 的新类型2.1RecordK, U将对象某些属性转换成另一个类型常见于回调场景回调返回值覆盖对象每个 key 的类型2.1ExcludeT, U将 T 中的 U 类型排除和 Extract 功能相反2.8ExtractT, UPick 的底层 API直到 2.8 才内置可以认为 Pick 是挑选对象的某些 keyExtract 是挑选 key 中的 key2.8NonNullableT排除 T 的null与undefined的可能性2.8ReturnTypeT获取函数 T 返回值的类型2.8InstanceTypeT获取一个构造函数类型的实例类型2.8OmitT, K原文档写作未内置从对象 T 中排除 key 是 K 的属性可用内置类型推导type OmitT, K PickT, Excludekeyof T, K3.5 起内置ReturnType 的实战价值打通 Redux Connect 与 React 组件原文档单独拿出ReturnType说明其重要性Redux 的Connect第一个参数是mapStateToProps这些 Props 会自动与 React Props 聚合。利用ReturnTypetypeof currentMapStateToProps拿到当前 Connect 注入给 Props 的类型就可以打通 Connect 与 React 组件的类型系统了。本仓库的两篇文档从实现角度给出了ReturnType的底层原理前沿技术/207.精读《Typescript infer 关键字》.md 指出官方正是用ReturnType作为infer的经典教学案例type ReturnTypeT T extends (...args: any[]) infer R ? R : any;——如果T满足函数结构则把函数返回值位置用infer R指代并返回。TS 类型体操/244.精读《Get return type, Omit, ReadOnly...》.md 通过手写MyReturnType演示函数返回类型早已被 TS 推导好如1 | 2我们要做的只是用infer把它抽出来。Omit的推导思路同样可以在该文档中找到完整实现type MyOmitT, K extends keyof T PickT, Excludekeyof T, K——这正是原文档给出的Omit组合公式。对 Generators 和 async/await 的类型定义TS 2.3 对 Generators 做了许多增强但实践中我们早已用async/await替代了它所以对 Generators 的增强可以忽略。需要重点注意的是对for..of语法的异步迭代支持async function f() { for await (const x of fn1()) { console.log(x); } }这会对可迭代对象的每一步进行异步迭代。注意与下面写法对比async function f() { for (const x of await fn2()) { console.log(x); } }二者的区别在于对于fn1它的返回值是可迭代对象且每个 item 类型都是Promise或 Generator因此需要for await逐项等待。对于fn2它自身是个异步函数返回值是可迭代的且每个 item 都不是异步的只需await拿到整体再普通迭代。举例function fn1() { return [Promise.resolve(1), Promise.resolve(2)]; } function fn2() { return [1, 2]; }顺带一提对Array.map的每一项进行异步等待的写法await Promise.all( arr.map(async item { return await item.run(); }) );如果要求执行顺序可以换成for..of语法因为数组类型是一种可迭代类型。泛型默认参数减少函数类型重载的代码量了解这个特性之前先回顾 TS 2.0 之前就支持的函数类型重载。JS 本身不支持方法重载Java 支持而 TS 类型系统一定程度在对标 Java。好在 JS 有一些偏方实现伪方法重载典型的是 redux 的createStoreexport default function createStore(reducer, preloadedState, enhancer) { if (typeof preloadedState function typeof enhancer undefined) { enhancer preloadedState; preloadedState undefined; } }JS 有办法支持伪重载TS 又补充了函数类型重载两者结合就等于 Java 方法重载declare function createStore( reducer: Reducer, preloadedState: PreloadedState, enhancer: Enhancer ); declare function createStore(reducer: Reducer, enhancer: Enhancer);可以清晰看到createStore想表现的是对参数个数的重载定义了函数类型重载后TS 会根据调用时传入的参数自动匹配对应的定义。而 TS 2.3 支持的泛型默认参数可以减少某些场景函数类型重载的代码量。比如下面的代码通过枚举表达了泛型默认值以及 U 与 T 之间可能存在的关系declare function create(): ContainerHTMLDivElement, HTMLDivElement[]; declare function createT extends HTMLElement(element: T): ContainerT, T[]; declare function createT extends HTMLElement, U extends HTMLElement( element: T, children: U[] ): ContainerT, U[];这些都可以用泛型默认参数解决declare function createT extends HTMLElement HTMLDivElement, U T[]( element?: T, children?: U ): ContainerT, U;尤其在 React 使用过程中如果用泛型默认值定义Component.. ComponentProps {}, State {} ..就可以实现以下等价效果class Component extends React.PureComponentany, any { //... } // 等价于 class Component extends React.PureComponent { //... }关于泛型默认参数的实战延伸TS 类型体操/243.精读《Pick, Awaited, If...》.md 给出过一个值得琢磨的对照type MyPickT, K extends keyof T keyof T——把K从写在对象描述里挪到泛型参数里并给定默认值即可让MyPickTodo在不传第二个参数时原封不动返回Todo功能上与{ [P in keyof T]: T[P] }完全一致却拥有更强的可拓展性。动态 Import 与 import() 类型TS 从 2.4 版本开始支持动态 Import。同时 Webpack 4.0 也支持了这个语法详见 前沿技术/47.精读《webpack4.0 升级指南》.md因此该语法正式可以用于生产环境const zipUtil await import(./utils/create-zip-file);准确地说动态 Import 实现于 webpack 2.1.0-beta.28最终在 TS 2.4 获得了语法支持。在 TS 2.9 版本开始支持了import()类型定义const zipUtil: typeof import(./utils/create-zip-file) await import(./utils/create-zip-file)也就是typeof可以作用于import()语法而不会真正引入 JS 内容。不过要注意import(./utils/create-zip-file)的路径需要可被推导要么存在这个 npm 模块要么是相对路径要么在tsconfig.json中定义了paths。好在import语法本身限制了路径必须是字面量使得自动推导的成功率非常高——只要是正确的代码几乎一定可以推导出来。这也是原文档推荐放弃require的另一个角度require的动态拼接路径无法获得同等的静态推导保证。Enum 类型支持字符串从 TypeScript 2.4 开始枚举类型支持使用字符串作为 valueenum Colors { Red RED, Green GREEN, Blue BLUE }原文档特别提醒这个功能在纯前端代码内可能没有用。因为在 TS 中所有使用enum的地方都建议用enum接收下面给出例子// 正确 { type: monaco.languages.types.Folder; } // 错误 { type: 75; }不仅是可读性问题enum对应的数字可能随版本变化直接写75的做法存在风险。但如果前后端存在交互前端不可能发送enum对象必须转化成数字——此时使用字符串作为 value 会更安全因为字符串字面量天然自描述、可读且不易混淆enum types { Folder FOLDER } fetch(/api?type${monaco.languages.types.Folder});数组类型可以明确长度定长元组最典型的是 chart 图数据经常是这种二维数组[[1, 5.5], [2, 3.7], [3, 2.0], [4, 5.9], [5, 3.9]]一般我们会这样描述其数据结构const data: number[][] [[1, 5.5], [2, 3.7], [3, 2.0], [4, 5.9], [5, 3.9]];在 TS 2.7 版本中可以更精确地描述每一项的类型与数组总长度interface ChartData extends Arraynumber { 0: number; 1: number; length: 2; }这种通过下标与length约束数组形态的能力正是元组Tuple类型的基础。本仓库 TS 类型体操/243.精读《Pick, Awaited, If...》.md 的LengthT一题进一步展示了该能力的边界T[length]对元组返回的是具体数值如4而对普通数组返回的是number——因为元组对 TS 而言长度可观测数组不可观测。自动类型推导typeof、instanceof 与 in自动类型推导类型收窄有两种经典形式。其一是typeoffunction foo(x: string | number) { if (typeof x string) { return x; // string } return x; // number }其二是instanceoffunction f1(x: B | C | D) { if (x instanceof B) { x; // B } else if (x instanceof C) { x; // C } else { x; // D } }在 TS 2.7 版本中新增了in的推导interface A { a: number; } interface B { b: string; } function foo(x: A | B) { if (a in x) { return x.a; } return x.b; }in推导解决了object类型的自动推导问题object既无法用keyof也无法用instanceof判定具体类型因此找到对象的特征 key 即可完成收窄。原文档强烈建议用in收窄取代as断言// Bad function foo(x: A | B) { // I know its A, but i cant describe it. (x as A).keyofA; } // Good function foo(x: A | B) { // I know its A, because it has property keyofA if (keyofA in x) { x.keyofA; } }总结以功能为纲的 TS 学习路线TypeScript 2.0–2.9 的文档整体读下来可以发现这段时期的版本演进有较强的连贯性严格模式与非空断言解决空值恐慌条件类型让类型系统获得分支与递归能力keyof 映射类型构建出工具类型全家桶泛型默认参数简化重载import()类型打通动态加载的类型推导。但我们可能并不习惯一步步学习新语法——新语法需要时间消化同时要连接到以往语法的上下文才能更好理解。所以原文档选择从功能角度而非版本角度梳理新特性这更符合学习习惯。另一个感悟是我们也许要用追月刊漫画的思维去学习新语言特别是 TS 这种正在发展中、迭代速度很快的语言。当新特性发布时最好的做法是快速将其固化为自己的知识而不是等它沉淀。如果想继续深化本主题可以在本仓库的 TS 类型体操 系列中逐一实战从 Pick, Awaited, If... 起步掌握keyof、映射类型与联合类型分发通过 Get return type, Omit, ReadOnly... 深入工具类型的组合与递归并结合 Typescript infer 关键字 理解条件类型中infer的推导机制与协变/逆变。这些练习正是把本文所讲硬知识转化为TS 思维的关键路径。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐roadmap.sh类型安全TypeScript严格模式roadmap.sh类型安全TypeScript严格模式 为什么需要严格模式 在大型开源项目如roadmap.sh中类型安全是保证代码质量和开发效率的关键文档教程知识库OpenFang 内置 TypeScript 专家技能解读从严格模式到类型级安全的 TypeScript 类型系统实战指南OpenFang 内置 TypeScript 专家技能解读从严格模式到类型级安全的 TypeScript 类型系统实战指南 导读 本文以 OpenFang 开人工智能大模型AI Agent自主智能体Agent 编排MCP Clients知识图谱dokploy类型安全TypeScript严格模式与类型定义dokploy类型安全TypeScript严格模式与类型定义 引言为什么类型安全对现代部署平台至关重要 在现代云原生应用部署领域类型安全不再是一个可选项后端前端云原生DevOps容器编排运维上一篇RMind 思维导图路线图与展望自定义主题、Minimap 等值得期待的新功能下一篇Lightdash 可视化系统架构解析从九层扩展模式到 Sankey 图的 BFS 循环流实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考