拓冰建站拓冰建站
首页 / 资讯中心 / 正文

TypeScript 2.1 特性全解析:keyof 查找类型、映射类型与工程化能力升级

文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载TypeScript 2.1 是类型系统走向成熟的关键版本它首次引入keyof索引类型查询与查找类型正式确立映射类型语法并将Partial、Readonly、Record、Pick纳入标准库同时带来对象扩展/rest 运算符、ES3/ES5 环境下的异步函数、tslib外部辅助库、配置继承extends等大量工程化改进。本文以 TypeScript 使用手册中文版的《TypeScript 2.1》发布说明为骨架结合仓库内的实用工具类型、高级类型、映射类型手册 v2、tsconfig.json 配置与破坏性改动等配套文档逐一展开每个新特性的语法、原理与实战用法帮助你理解这些至今仍被广泛使用的类型工具背后的设计动机。keyof与查找类型把属性名变成类型在 JavaScript 中以属性名称作为参数的 API 相当普遍如get、set、pick、omit这类工具函数但在此之前TypeScript 一直缺乏一种方式来表达属性名与属性值之间的类型关系。TypeScript 2.1 为此引入了索引类型查询indexed type query即keyof。keyof T产生的类型是T的全部属性名组成的联合类型且被认为是string的子类型interface Person { name: string; age: number; location: string; } type K1 keyof Person; // name | age | location type K2 keyof Person[]; // length | push | pop | concat | ... type K3 keyof { [x: string]: Person }; // string从示例可见对普通接口取keyof得到属性名字面量联合对数组取keyof得到的是数组的内置方法名如push、pop、concat对具有字符串索引签名的对象取keyof则退化为string。这一特性与高级类型中介绍的索引签名、联合类型等机制共同构成了现代类型操作的基础。与之相对应的是索引访问类型indexed access type也称查找类型lookup type。语法上它看起来像元素访问但出现在类型位置上type P1 Person[name]; // string type P2 Person[name | age]; // string | number type P3 string[charAt]; // (pos: number) string type P4 string[][push]; // (...items: string[]) number type P5 string[][0]; // string索引访问支持传入单个字面量Person[name]、字面量联合Person[name | age]、方法名string[charAt]甚至数字索引string[][0]结果是对应成员的类型。将keyof与查找类型组合到泛型约束中就能写出真正类型安全的属性读写函数function getPropertyT, K extends keyof T(obj: T, key: K) { return obj[key]; // 推断类型是 T[K] } function setPropertyT, K extends keyof T(obj: T, key: K, value: T[K]) { obj[key] value; } let x { foo: 10, bar: hello! }; let foo getProperty(x, foo); // number let bar getProperty(x, bar); // string let oops getProperty(x, wargarbl); // 错误wargarbl 不存在于 foo | bar 中 setProperty(x, foo, string); // 错误属性 foo 的类型是 number 而非 string这里K extends keyof T保证key一定是T的合法属性名而返回值/写入值类型被精确收窄为T[K]——传入foo时返回number传入bar时返回string传错属性名或值类型不匹配都会在编译期直接报错。这套模式正是后来pick、omit等工具类型与Object.keys类型安全封装的理论基础。映射类型批量变换类型的属性一个常见的需求是基于现有类型生成每个属性都可选的新类型。假设有Personinterface Person { name: string; age: number; location: string; }手动写出可选版本既冗长又难以与源类型保持同步interface PartialPerson { name?: string; age?: number; location?: string; }映射类型mapped type将这个过程变成对Person的一次广义变换它通过遍历字面量类型的集合通常是keyof T为新的对象类型计算出一组属性。可以类比 Python 中的列表推导式——只是它生成的不是列表元素而是类型属性type PartialT { [P in keyof T]?: T[P]; }; type PartialPerson PartialPerson;语法[P in keyof T]?: T[P]的含义是对T的每个属性名P生成同名属性值类型为T[P]并统一加上可选标记?。除Partial外映射类型可以表达许多有用的类型变换// 保持类型相同但每个属性是只读的。 type ReadonlyT { readonly [P in keyof T]: T[P]; }; // 相同的属性名称但使值是一个 Promise而不是一个具体的值 type DeferredT { [P in keyof T]: PromiseT[P]; }; // 为 T 的属性添加代理 type ProxifyT { [P in keyof T]: { get(): T[P]; set(v: T[P]): void }; };ReadonlyT为每个属性加readonlyDeferredT把每个属性值包装成Promise非常适合异步加载场景ProxifyT则为每个属性生成一对get/set访问器签名适合代理模式封装。后续版本在映射类型上又加入了readonly、?修饰符的/-增删语法详见映射类型手册 v2其根源正是 2.1 确立的这套[P in keyof T]机制。Partial、Readonly、Record与Pick首批标准库工具类型Partial和Readonly因为足够通用从 TypeScript 2.1 起默认包含在标准库lib中全局可见、无需引入。它们可以描述常见的 JS 运行时行为function assignT(obj: T, props: PartialT): void; function freezeT(obj: T): ReadonlyT;assign用PartialT表达只允许传入obj已有属性的子集freeze用ReadonlyT表达冻结后所有属性不可写——这正是Object.freeze的类型签名。关于这四个工具类型的完整定义与更多示例可参阅仓库中的实用工具类型一章其中Partial、Readonly、Record、Pick均标注为 TypeScript 2.1 引入。2.1 同时带来了另外两个工具类型Record与Pick。Pick从T中选取一组属性K构造新类型// 从 T 中选取一组属性 K declare function pickT, K extends keyof T(obj: T, ...keys: K[]): PickT, K; const nameAndAgeOnly pick(person, name, age); // { name: string, age: number }Record则以一组键K为属性名、以T为值类型构造对象类型常与keyof和映射一起配合使用// 对于类型 T 的每个属性 K将其转换为 U function mapObjectK extends string | number, T, U( obj: RecordK, T, f: (x: T) U ): RecordK, U; const names { foo: hello, bar: world, baz: bye }; const lengths mapObject(names, s s.length); // { foo: number, bar: number, baz: number }RecordK, T要求K必须是string | number的子类型从而保证键的合法性mapObject借助它实现了逐属性映射值类型的类型安全版本。Record的典型用法还包括以字面量联合为键构造枚举字典例如type Page home | about | contact搭配RecordPage, PageInfo见实用工具类型。对象扩展运算符与 rest 运算符TypeScript 2.1 带来了 ES.next 提案中对象**扩展运算符spread**与rest 运算符的支持。类似于数组展开展开对象可以方便地得到浅拷贝let copy { ...original };也可以一次性合并多个对象——以下merged将具有来自foo、bar和baz的全部属性let merged { ...foo, ...bar, ...baz };还可以在展开的同时重写已有属性、添加新属性let obj { x: 1, y: string }; var newObj { ...obj, z: 3, y: 4 }; // { x: number, y: number, z: number }这里y从string被覆盖为numberz是新增属性。展开操作的书写顺序决定了最终结果对象中的属性归属同名属性靠后的展开或显式字段会覆盖靠前的。与对象扩展相对的是对象 rest 运算符用于在解构时提取剩余属性let obj { x: 1, y: 1, z: 1 }; let { z, ...obj1 } obj; obj1; // {x: number, y: number};z被单独解构出去obj1则收集了其余属性。对象 rest/spread 在 React 组件的 props 透传、Immutable 式状态更新等场景中极为常用其类型层面的约束重写属性后的联合结果类型由编译器自动推导。低版本异步函数async/await 下沉到 ES3 与 ES5async/await在 TypeScript 2.1 之前就已支持但只能编译为 ES6/ES2015 目标。TypeScript 2.1 使其可以在 ES3 与 ES5 运行时上使用意味着无论目标环境多么老旧都可以安全使用异步语法。使用前需要注意两点运行时必须提供全局 ECMAScript 兼容的Promise——可能需要引入Promise的 polyfill如es6-promise或依赖运行时自带的实现通过lib编译参数告知 TypeScriptPromise可用例如dom, es2015或dom, es2015.promise, es5。完整的配置与示例tsconfig.json{ compilerOptions: { lib: [dom, es2015.promise, es5] } }dramaticWelcome.tsfunction delay(milliseconds: number) { return new Promisevoid(resolve { setTimeout(resolve, milliseconds); }); } async function dramaticWelcome() { console.log(Hello); for (let i 0; i 3; i) { await delay(500); console.log(.); } console.log(World!); } dramaticWelcome();lib数组里es2015.promise只引入Promise相关声明而不引入整套 ES2015 库配合es5即可让编译器在 ES5 目标下正确认识Promise类型。编译后在 ES3/ES5 引擎上运行应能看到Hello、间隔出现的三个.、最后World!的正确输出顺序——编译器会生成基于状态机的降级代码而不是原样保留await。支持外部辅助库tslib告别重复注入TypeScript 编译器会向输出文件注入若干辅助函数helpers例如类继承用的__extends、JSX/对象展开用的__assign、异步函数用的__awaiter。2.1 之前只有两种选择各有痛点在每一个需要辅助库的文件中都注入一份辅助函数——导致每个输出文件都携带重复代码对在意包体积的库作者而言是明显负担使用--noEmitHelpers完全不生成辅助函数——代码虽小但客户必须自行维护这些辅助函数。TypeScript 2.1 引入了第三种方案把辅助库作为单独模块一次性加入项目编译器按需require导入。步骤如下。首先安装tslibnpm install tslib然后使用--importHelpers编译tsc --module commonjs --importHelpers a.ts对于如下输入export const o { a: 1, name: o }; export const copy { ...o };生成的.js文件将包含对tslib的导入并用__assign辅助函数替代内联的对象展开use strict; var tslib_1 require(tslib); exports.o { a: 1, name: o }; exports.copy tslib_1.__assign({}, exports.o);这样辅助代码在整个项目中只存在一份tslib模块内每个输出文件仅多一行require包尺寸与可维护性同时得到改善。无类型导入允许导入没有.d.ts的 JavaScript 模块TypeScript 历来对模块导入相当严格这是为了避免拼写错误并防止用户误用模块。但实际项目中经常需要导入那些**没有类型声明.d.ts**的既有 JavaScript 模块——在 2.1 之前这会直接报错。从 TypeScript 2.1 起你可以导入 JavaScript 模块而不需要类型声明// 只要 node_modules/asdf/index.js 存在即可通过编译 import { x } from asdf;需要注意的优先级与限制如果类型声明存在——无论是declare module foo { ... }这样的环境模块声明还是node_modules/types/foo下的声明文件——它们仍然优先被采用对于完全没有声明文件的模块导入若启用了--noImplicitAny仍会被标记为错误因为导入的名字x隐式为any。也就是说2.1 放宽的是无声明也可导入的边界而严格性开关--noImplicitAny依然生效。关于模块解析如何逐级查找node_modules、types以及package.json的types字段可进一步阅读模块解析一章。新增编译目标--target ES2016、--target ES2017与--target ESNextTypeScript 2.1 为tsc增加了三个新的编译目标取值--target ES2016指示编译器不要降级编译ES2016 特有的特性例如**幂运算符--target ES2017指示编译器不要降级 ES2017 特有的特性例如async/await--target ESNext对应最新的 TC39 ES 提议特性支持让编译器紧跟提案演进。这三个目标让不同代码库可以按各自的运行环境选择降级到哪一代语法例如目标环境已原生支持幂运算符或异步函数时就无需再付出额外的降级代码体积。改进的any类型推断基于后续赋值动态收窄在 2.1 之前如果 TypeScript 无法确定变量类型就会直接选择anylet x; // 隐式 any let y []; // 隐式 any[] let z: any; // 显式 any.TypeScript 2.1 改变了这一行为不再简单地给变量标为any而是基于后续赋值持续推断并更新类型。注意此行为仅在设置了--noImplicitAny编译参数时才启用。let x; // 你仍然可以给 x 赋任何你需要的值。 x () 42; // 在刚赋值后TypeScript 2.1 知道 x 的类型是 () number。 let y x(); // 感谢现在它会告诉你你不能把一个数字加到一个函数上 console.log(x y); // ~~~~~ // 错误运算符 不能应用于类型 () number 和 number。 // TypeScript 仍然允许你给 x 赋任何你需要的值。 x Hello world!; // 并且现在它也知道 x 是 string 类型的 x.toLowerCase();x先是函数类型之后被重新赋值为字符串编译器随之把x的类型更新为string于是x.toLowerCase()可以正常调用而x y这类非法运算会在编译期报错。空数组也得到同样的动态跟踪没有类型注解且初始值为[]的变量被视为隐式any[]并会根据x.push(value)、x.unshift(value)、x[n] value等操作加入的元素不断演化自身类型function f1() { let x []; x.push(5); x[1] hello; x.unshift(true); return x; // (string | number | boolean)[] } function f2() { let x null; if (cond()) { x []; while (cond()) { x.push(hello); } } return x; // string[] | null }f1中依次压入number、string、boolean返回类型被推导为三者联合的数组f2中x在null与string[]之间演化返回类型为string[] | null——即使没有写任何类型注解编译器也能给出相当精确的结果。隐式 any 错误变少了这一改进的额外红利是启用--noImplicitAny时你会看到更少的隐式any错误。隐式any错误只会在编译器确实无法确定一个无类型注解变量的类型时才报告function f3() { let x []; // 错误当变量 x 类型无法确定时它隐式具有 any[] 类型。 x.push(5); function g() { x; // 错误变量 x 隐式具有 any[] 类型。 } }f3中x的演化信息没有被充分建立且被闭包g引用编译器无法确定类型于是仍然报告隐式any[]错误——这正是动态推断与严格检查并存的设计。更好的字面量类型推断字符串、数字、布尔字面量类型如abc、1、true此前仅在存在显式类型注解时才会被推断出来。从 TypeScript 2.1 起字面量类型总是按规则推断不带类型注解的const变量或readonly属性推断为字面量初始化类型本身已初始化且不带类型注解的let变量、var变量、形参或非readonly属性推断为初始值的扩展字面量类型——字符串字面量扩展为string数字字面量扩展为numbertrue/false扩展为boolean枚举字面量扩展为枚举类型。const c1 1; // Type 1 const c2 c1; // Type 1 const c3 abc; // Type abc const c4 true; // Type true const c5 cond ? 1 : abc; // Type 1 | abc let v1 1; // Type number let v2 c2; // Type number let v3 c3; // Type string let v4 c4; // Type boolean let v5 c5; // Type number | string注意c2 c1仍是字面量1const 传播而v2 c2则扩展为number。字面量类型的扩展还可以通过显式类型注解来控制当一个无注解的const局部变量的初始化表达式被推断为字面量类型时从它赋值的var变量获得扩展字面量类型而当const局部变量带显式字面量类型注解时从它赋值的var变量获得非扩展字面量类型const c1 hello; // Widening type hello let v1 c1; // Type string const c2: hello hello; // Type hello let v2 c2; // Type hello需要留意的是这一变化也带来一处破坏性改动const变量和readonly属性默认推断为字面量类型后/!等比较可能产生更严格的结果——例如const DEBUG true现在类型是true而非booleanDEBUG false会直接报运算符 不能应用于 true 和 false如需刻意放宽可用const DEBUG booleantrue显式声明为boolean。详见破坏性改动TypeScript 2.1。将基类构造函数的返回值作为this在 ES2015 语义中如果构造函数返回一个对象该返回值会隐式替换调用方super()的调用者的this。TypeScript 2.1 遵循这一语义捕获任何可能的super()返回值并替换为this。此更改让使用自定义元素Custom Elements成为可能——浏览器分配的元素可以由用户编写的构造函数来初始化。class Base { x: number; constructor() { // 返回一个除 this 之外的新对象 return { x: 1, }; } } class Derived extends Base { constructor() { super(); this.x 2; } }生成的 ES5 代码var Derived (function (_super) { __extends(Derived, _super); function Derived() { var _this _super.call(this) || this; _this.x 2; return _this; } return Derived; })(Base);可以看到_super.call(this)的返回值被存入局部变量_this构造函数体内对this的访问全部改写为_this并且构造函数显式返回_this以保证继承链正确。配套说明见破坏性改动TypeScript 2.1这条语义变更同时意味着继承内置类Error、Array、Map等在 ES5 目标下可能不再可靠工作——因为这些内置构造函数的原型链依赖 ES6 的new.target调整而 ES5 调用方式无法保证new.target。典型后果是子类实例上的方法可能为undefined、instanceof判断失效(new FooError()) instanceof FooError返回false。官方推荐的规避方式是在super(...)之后手动修复原型class FooError extends Error { constructor(m: string) { super(m); // Set the prototype explicitly. Object.setPrototypeOf(this, FooError.prototype); } sayHello() { return hello this.message; } }不支持Object.setPrototypeOf的运行时可退而使用__proto__。这一节与发布说明正文形成互补前者解释为什么生成代码变成这样后者给出迁移与修复建议。配置继承用extends复用tsconfig.json大型项目常常需要多份构建配置ES5 与 ES2015、调试与生产、CommonJS 与 System 模块等而它们之间往往只有少数几个选项不同维护多份几乎重复的tsconfig.json非常麻烦。TypeScript 2.1 为此引入了extends配置继承规则如下extends是tsconfig.json中新的顶级属性与compilerOptions、files、include、exclude并列extends的值是一个字符串指向要继承的其它tsconfig.json的路径可省略.json扩展名先加载被继承基础文件中的配置再由继承文件中的配置重写如果发现循环继承编译器报告错误继承配置中的files、include、exclude会覆盖基础配置中对应的值而非合并配置文件中出现的所有相对路径都相对于它们各自所在的配置文件来解析。一个典型的继承链示例configs/base.json{ compilerOptions: { allowJs: true, noImplicitAny: true, strictNullChecks: true } }configs/tests.json测试专用配置覆盖文件范围与部分编译选项{ compilerOptions: { preserveConstEnums: true, stripComments: false, sourceMaps: true }, exclude: [ ../tests/baselines, ../tests/scenarios ], include: [ ../tests/**/*.ts ] }tsconfig.json主配置继承基础配置{ extends: ./configs/base, files: [ main.ts, supplemental.ts ] }tsconfig.nostrictnull.json针对特殊场景覆盖单个选项{ extends: ./tsconfig, compilerOptions: { strictNullChecks: false } }注意上例中configs/tests.json的include/exclude里../tests/...是相对于configs/目录解析的。这套机制此后成为多环境配置的标准做法更完整的继承说明与示例可参考tsconfig.json一章中使用extends继承配置一节。新编译参数--alwaysStrict--alwaysStrict使编译器以严格模式解析所有代码即在解析层面启用 ES5 strict mode 语义在每一个生成文件上输出use strict;指令。模块代码会自动按严格模式解析因此该参数主要面向非模块脚本代码——如果你希望所有输出文件都显式携带use strict;建议加上此选项。小结TypeScript 2.1 同时奠定了类型系统的基础设施与工程化能力keyof/查找类型/映射类型三个机制催生了Partial、Readonly、Record、Pick等至今仍写进无数代码库的标准工具类型对象 spread/rest 与低版本 async/await 直接改善日常开发体验tslib、配置继承与--alwaysStrict则让大型项目的构建更可控。若要进一步探索这些特性的现代形态映射类型修饰符的增删、Omit等后继工具类型可继续阅读仓库中的实用工具类型、映射类型手册 v2与高级类型升级到 2.1 时需要注意的行为变化请对照破坏性改动TypeScript 2.1逐一排查。赞分享文档教程【免费下载链接】TypeScriptTypeScript 使用手册中文版翻译。http://www.typescriptlang.org项目地址https://gitcode.com/gh_mirrors/typ/TypeScript点击查看免费下载相关推荐Redocusaurus在Docusaurus中完美集成OpenAPI文档的终极指南Redocusaurus在Docusaurus中完美集成OpenAPI文档的终极指南 Redocusaurus是一个专为Docusaurus设计的预设工具能TypeScript 映射类型Mapped Types实战指南从 keyof 到内置工具类型的类型转换TypeScript 映射类型Mapped Types实战指南从 keyof 到内置工具类型的类型转换 映射类型Mapped Types是 TypeS文档教程TypeScript 映射类型Mapped Types实战指南用 keyof 构建类型变换、修饰符与 key 重映射TypeScript 映射类型Mapped Types实战指南用 keyof 构建类型变换、修饰符与 key 重映射 映射类型Mapped Types文档教程上一篇QHotkey多线程使用指南如何在子线程中安全注册全局热键下一篇Floccus众筹成功案例OpenCollective资金使用报告创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门