keyof 和查询类型 (Lookup Types)
在 JavaScript 中,经常会出现期望以属性名作为参数的 API,但到目前为止,还无法表达这些 API 中所存在的类型关系。
引入索引类型查询或 keyof;索引类型查询 keyof T 会产生 T 的允许属性名类型。keyof T 类型被视为 string 的子类型。
示例
tsinterface 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
与其对应的概念是索引访问类型,也称为查询类型 (lookup types)。在语法上,它们看起来完全像元素访问,但却作为类型书写。
示例
tstype P1 = Person["name"]; // stringtype P2 = Person["name" | "age"]; // string | numbertype P3 = string["charAt"]; // (pos: number) => stringtype P4 = string[]["push"]; // (...items: string[]) => numbertype P5 = string[][0]; // string
您可以将此模式与类型系统的其他部分结合使用,以获得类型安全的查询。
tsfunction getProperty<T, K extends keyof T>(obj: T, key: K) {return obj[key]; // Inferred type is T[K]}function setProperty<T, 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"); // numberlet bar = getProperty(x, "bar"); // stringlet oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"setProperty(x, "foo", "string"); // Error!, string expected number
映射类型 (Mapped Types)
一个常见的任务是获取一个现有类型,并使其每个属性完全可选。假设我们有一个 Person 类型。
tsinterface Person {name: string;age: number;location: string;}
它的部分版本将是:
tsinterface PartialPerson {name?: string;age?: number;location?: string;}
使用映射类型,PartialPerson 可以作为对 Person 类型的通用转换来编写:
tstype Partial<T> = {[P in keyof T]?: T[P];};type PartialPerson = Partial<Person>;
映射类型是通过获取字面量类型的并集,并为新的对象类型计算一组属性而产生的。它们就像 Python 中的列表推导式,但它们产生的不是列表中的新元素,而是类型中的新属性。
除了 Partial,映射类型还可以表示许多有用的类型转换。
ts// Keep types the same, but make each property to be read-only.type Readonly<T> = {readonly [P in keyof T]: T[P];};// Same property names, but make the value a promise instead of a concrete onetype Deferred<T> = {[P in keyof T]: Promise<T[P]>;};// Wrap proxies around properties of Ttype Proxify<T> = {[P in keyof T]: { get(): T[P]; set(v: T[P]): void };};
Partial, Readonly, Record 和 Pick
如前所述,Partial 和 Readonly 是非常有用的结构。您可以使用它们来描述一些常见的 JS 例程,例如:
tsfunction assign<T>(obj: T, props: Partial<T>): void;function freeze<T>(obj: T): Readonly<T>;
因此,它们现在被默认包含在标准库中。
我们还包含了另外两种实用类型:Record 和 Pick。
ts// From T pick a set of properties Kdeclare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
ts// For every properties K of type T, transform it to Ufunction mapObject<K extends string, T, U>(obj: Record<K, T>,f: (x: T) => U): Record<K, U>;const names = { foo: "hello", bar: "world", baz: "bye" };const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
对象展开 (Spread) 与剩余 (Rest)
TypeScript 2.1 带来了对 ESnext 展开与剩余操作的支持。
与数组展开类似,展开对象可以方便地进行浅拷贝:
tslet copy = { ...original };
同样,您可以合并多个不同的对象。在下面的示例中,merged 将拥有来自 foo、bar 和 baz 的属性。
tslet merged = { ...foo, ...bar, ...baz };
您还可以覆盖现有属性并添加新属性:
tslet obj = { x: 1, y: "string" };var newObj = { ...obj, z: 3, y: 4 }; // { x: number, y: number, z: number }
指定展开操作的顺序决定了最终对象中包含哪些属性;后续展开的属性会“覆盖”之前创建的属性。
对象剩余操作是对象展开的对偶,它们可以提取在解构元素时未被选中的任何额外属性。
tslet obj = { x: 1, y: 1, z: 1 };let { z, ...obj1 } = obj;obj1; // {x: number, y:number};
向下兼容异步函数
此功能在 TypeScript 2.1 之前就已经得到支持,但仅限于目标环境为 ES6/ES2015 时。TypeScript 2.1 将此功能带到了 ES3 和 ES5 运行时,这意味着无论您使用什么环境,都可以自由地利用它。
注意:首先,我们需要确保我们的运行时具有全局可用的、符合 ECMAScript 标准的
Promise。这可能涉及获取一个Promise的 polyfill,或者依赖于您所目标运行时中可能已有的Promise。我们还需要通过将lib选项设置为类似"dom", "es2015"或"dom", "es2015.promise", "es5"的值,确保 TypeScript 知道Promise的存在。
示例
tsconfig.json
{"": {"": ["dom", "es2015.promise", "es5"]}}
dramaticWelcome.ts
tsfunction delay(milliseconds: number) {return new Promise<void>(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();
编译并运行输出代码应在 ES3/ES5 引擎上产生正确的行为。
支持外部辅助函数库 (tslib)
TypeScript 会注入少量辅助函数,例如用于继承的 __extends、用于对象字面量和 JSX 元素中展开运算符的 __assign,以及用于异步函数的 __awaiter。
之前有两种选择:
- 在每个需要它们的文件中注入辅助函数,或者
- 使用
noEmitHelpers完全不包含辅助函数。
这两种选择都无法满足需求;在每个文件中打包辅助函数对于试图保持包体积小的客户来说是一个痛点。而不包含辅助函数意味着客户必须维护自己的辅助函数库。
TypeScript 2.1 允许您在项目中将这些文件作为一个独立的模块包含一次,编译器将根据需要为它们发出导入语句。
首先,安装 tslib 实用库:
shnpm install tslib
其次,使用 importHelpers 编译您的文件:
shtsc --module commonjs --importHelpers a.ts
因此,给定以下输入,生成的 .js 文件将包含对 tslib 的导入,并使用其中的 __assign 辅助函数,而不是将其内联。
tsexport const o = { a: 1, name: "o" };export const copy = { ...o };
js"use strict";var tslib_1 = require("tslib");exports.o = { a: 1, name: "o" };exports.copy = tslib_1.__assign({}, exports.o);
无类型导入 (Untyped imports)
一直以来,TypeScript 对如何导入模块的要求非常严格。这是为了避免拼写错误并防止用户不正确地使用模块。
然而,很多时候,您可能只是想导入一个现有的模块,该模块可能没有自己的 .d.ts 文件。以前这会被视为错误。从 TypeScript 2.1 开始,这变得容易多了。
使用 TypeScript 2.1,您可以导入 JavaScript 模块而无需类型声明。如果存在类型声明(例如 declare module "foo" { ... } 或 node_modules/@types/foo),它仍然具有优先权。
在 noImplicitAny 下,对没有声明文件的模块的导入仍将被标记为错误。
示例
ts// Succeeds if `node_modules/asdf/index.js` existsimport { x } from "asdf";
支持 --target ES2016, --target ES2017 和 --target ESNext
TypeScript 2.1 支持三个新的目标值:--target ES2016, --target ES2017 和 --target ESNext。
使用 --target ES2016 将指示编译器不要转换 ES2016 特有的特性,例如 ** 运算符。
同样,--target ES2017 将指示编译器不要转换 ES2017 特有的特性,如 async/await。
--target ESNext 针对的是最新支持的 ES 提案特性。
改进的 any 推断
以前,如果 TypeScript 无法弄清楚变量的类型,它会选择 any 类型。
tslet x; // implicitly 'any'let y = []; // implicitly 'any[]'let z: any; // explicitly 'any'.
从 TypeScript 2.1 开始,TypeScript 不再仅仅选择 any,而是根据您后续分配的内容来推断类型。
此功能仅在设置了 noImplicitAny 时启用。
示例
tslet x;// You can still assign anything you want to 'x'.x = () => 42;// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.let y = x();// Thanks to that, it will now tell you that you can't add a number to a function!console.log(x + y);// ~~~~~// Error! Operator '+' cannot be applied to types '() => number' and 'number'.// TypeScript still allows you to assign anything you want to 'x'.x = "Hello world!";// But now it also knows that 'x' is a 'string'!x.toLowerCase();
现在也对空数组执行了相同的跟踪。
声明时没有类型注解且初始值为 [] 的变量被视为隐式 any[] 变量。但是,随后的每个 x.push(value)、x.unshift(value) 或 x[n] = value 操作都会根据添加的元素演变变量的类型。
tsfunction 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}
隐式 any 错误
这带来的一个巨大好处是,在使用 noImplicitAny 运行时,您会看到少得多的隐式 any 错误。隐式 any 错误仅在编译器在没有类型注解的情况下无法得知变量类型时报告。
示例
tsfunction f3() {let x = []; // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined.x.push(5);function g() {x; // Error: Variable 'x' implicitly has an 'any[]' type.}}
更好的字面量类型推断
字符串、数字和布尔字面量类型(例如 "abc", 1 和 true)以前仅在存在显式类型注解时才会推断。从 TypeScript 2.1 开始,字面量类型始终会被推断为 const 变量和 readonly 属性。
对于没有类型注解的 const 变量或 readonly 属性,推断出的类型是字面量初始值设定项的类型。对于带有初始值设定项且没有类型注解的 let 变量、var 变量、参数或非 readonly 属性,推断出的类型是初始值设定项的扩展字面量类型。其中字符串字面量类型的扩展类型是 string,数字字面量类型是 number,true 或 false 是 boolean,枚举字面量类型是其所属的枚举。
示例
tsconst c1 = 1; // Type 1const c2 = c1; // Type 1const c3 = "abc"; // Type "abc"const c4 = true; // Type trueconst c5 = cond ? 1 : "abc"; // Type 1 | "abc"let v1 = 1; // Type numberlet v2 = c2; // Type numberlet v3 = c3; // Type stringlet v4 = c4; // Type booleanlet v5 = c5; // Type number | string
字面量类型的扩展可以通过显式类型注解进行控制。具体来说,当在没有类型注解的 const 位置推断出字面量类型的表达式时,该 const 变量会推断出扩展的字面量类型。但当 const 位置有显式的字面量类型注解时,该 const 变量会得到一个非扩展的字面量类型。
示例
tsconst c1 = "hello"; // Widening type "hello"let v1 = c1; // Type stringconst c2: "hello" = "hello"; // Type "hello"let v2 = c2; // Type "hello"
使用 super 调用返回的值作为 'this'
在 ES2015 中,返回对象的构造函数隐式地将 this 值替换为任何调用 super() 的对象。因此,必须捕获 super() 的任何潜在返回值并将其替换为 this。此更改使得可以使用 Custom Elements,它利用这一点来使用用户编写的构造函数初始化浏览器分配的元素。
示例
tsclass Base {x: number;constructor() {// return a new object other than `this`return {x: 1};}}class Derived extends Base {constructor() {super();this.x = 2;}}
生成:
jsvar Derived = (function(_super) {__extends(Derived, _super);function Derived() {var _this = _super.call(this) || this;_this.x = 2;return _this;}return Derived;})(Base);
此更改会导致扩展内置类(如
Error,Array,Map等)的行为发生重大变化。请参阅扩展内置类破坏性更改文档以了解更多详细信息。
配置继承
通常,一个项目有多个输出目标,例如 ES5 和 ES2015、调试和生产,或 CommonJS 和 System;在这两个目标之间只有少数配置选项会发生变化,维护多个 tsconfig.json 文件可能会很麻烦。
TypeScript 2.1 支持使用 extends 继承配置,其中:
extends是tsconfig.json中的一个新顶级属性(与compilerOptions、files、include和exclude并列)。extends的值必须是一个字符串,包含要继承的另一个配置文件的路径。- 基础文件的配置首先被加载,然后被继承配置文件中的配置所覆盖。
- 不允许在配置文件之间存在循环引用。
- 继承配置文件中的
files、include和exclude会覆盖基础配置文件中的对应内容。 - 配置文件中发现的所有相对路径都将相对于它们所在的配置文件进行解析。
示例
configs/base.json:
{"": {"": true,"": true}}
tsconfig.json:
{"": "./configs/base","": ["main.ts", "supplemental.ts"]}
tsconfig.nostrictnull.json:
{"": "./tsconfig","": {"": false}}
新标志 --alwaysStrict
使用 alwaysStrict 调用编译器会:
- 以严格模式解析所有代码。
- 在每个生成文件的顶部写入
"use strict";指令。
模块会自动在严格模式下解析。建议将此新标志用于非模块代码。