strictBindCallApply
TypeScript 3.2 引入了一个新的 strictBindCallApply 编译器选项(属于 strict 选项族),使用该选项后,函数对象上的 bind、call 和 apply 方法将被强类型化并受到严格检查。
tsfunction foo(a: number, b: string): string {return a + b;}let a = foo.apply(undefined, [10]); // error: too few argumentslet b = foo.apply(undefined, [10, 20]); // error: 2nd argument is a numberlet c = foo.apply(undefined, [10, "hello", 30]); // error: too many argumentslet d = foo.apply(undefined, [10, "hello"]); // okay! returns a string
这是通过在 lib.d.ts 中引入两个新类型 CallableFunction 和 NewableFunction 来实现的。这些类型分别包含针对常规函数和构造函数方法的 bind、call 和 apply 的专用泛型方法声明。这些声明使用泛型剩余参数(参见 #24897)以强类型方式捕获并反映参数列表。在 strictBindCallApply 模式下,这些声明将取代 Function 类型提供的(非常宽松的)声明。
注意事项
由于更严格的检查可能会揭示以前未报告的错误,因此在 strict 模式下,这是一个破坏性变更。
此外,此新功能的另一个注意事项是,由于某些限制,bind、call 和 apply 尚无法完全模拟泛型函数或具有重载的函数。当在泛型函数上使用这些方法时,类型参数将被替换为空对象类型 ({});当在具有重载的函数上使用时,仅会模拟最后一个重载。
对象字面量中的泛型展开表达式
在 TypeScript 3.2 中,对象字面量现在允许使用泛型展开表达式,这些表达式现在会产生交叉类型,类似于 Object.assign 函数和 JSX 字面量。例如:
tsfunction taggedObject<T, U extends string>(obj: T, tag: U) {return { ...obj, tag }; // T & { tag: U }}let x = taggedObject({ x: 10, y: 20 }, "point"); // { x: number, y: number } & { tag: "point" }
属性赋值和非泛型展开表达式在泛型展开表达式的两侧会尽可能地合并。例如:
tsfunction foo1<T>(t: T, obj1: { a: string }, obj2: { b: string }) {return { ...obj1, x: 1, ...t, ...obj2, y: 2 }; // { a: string, x: number } & T & { b: string, y: number }}
非泛型展开表达式继续像以前一样处理:调用和构造签名被剥离,仅保留非方法属性,对于同名属性,使用最右侧属性的类型。这与交叉类型形成对比,交叉类型会连接调用和构造签名、保留所有属性,并对同名属性的类型取交集。因此,当通过泛型类型实例化创建相同类型的展开时,可能会产生不同的结果。
tsfunction spread<T, U>(t: T, u: U) {return { ...t, ...u }; // T & U}declare let x: { a: string; b: number };declare let y: { b: string; c: boolean };let s1 = { ...x, ...y }; // { a: string, b: string, c: boolean }let s2 = spread(x, y); // { a: string, b: number } & { b: string, c: boolean }let b1 = s1.b; // stringlet b2 = s2.b; // number & string
泛型对象剩余变量和参数
TypeScript 3.2 还允许从泛型变量中解构剩余绑定。这是通过使用 lib.d.ts 中预定义的 Pick 和 Exclude 辅助类型,并结合使用相关的泛型类型以及解构模式中其他绑定的名称来实现的。
tsfunction excludeTag<T extends { tag: string }>(obj: T) {let { tag, ...rest } = obj;return rest; // Pick<T, Exclude<keyof T, "tag">>}const taggedPoint = { x: 10, y: 20, tag: "point" };const point = excludeTag(taggedPoint); // { x: number, y: number }
BigInt
BigInt 是 ECMAScript 中一项即将推出的提案的一部分,它允许我们模拟理论上任意大的整数。TypeScript 3.2 带来了对 BigInt 的类型检查,以及在以 esnext 为目标时对 BigInt 字面量的输出支持。
TypeScript 中的 BigInt 支持引入了一种名为 bigint(全小写)的新基本类型。你可以通过调用 BigInt() 函数或在任何整数数值字面量末尾添加 n 来书写 BigInt 字面量,从而获得 bigint 类型。
tslet foo: bigint = BigInt(100); // the BigInt functionlet bar: bigint = 100n; // a BigInt literal// *Slaps roof of fibonacci function*// This bad boy returns ints that can get *so* big!function fibonacci(n: bigint) {let result = 1n;for (let last = 0n, i = 0n; i < n; i++) {const current = result;result += last;last = current;}return result;}fibonacci(10000n);
虽然你可能认为 number 和 bigint 之间有密切的交互,但两者属于不同的领域。
tsdeclare let foo: number;declare let bar: bigint;foo = bar; // error: Type 'bigint' is not assignable to type 'number'.bar = foo; // error: Type 'number' is not assignable to type 'bigint'.
正如 ECMAScript 规范所要求的那样,在算术运算中混合使用 number 和 bigint 是错误的。你必须显式地将值转换为 BigInt。
tsconsole.log(3.141592 * 10000n); // errorconsole.log(3145 * 10n); // errorconsole.log(BigInt(3145) * 10n); // okay!
同样需要注意的是,使用 typeof 运算符时,bigint 会产生一个新的字符串:字符串 "bigint"。因此,TypeScript 会如你所愿,正确地使用 typeof 进行类型收窄。
tsfunction whatKindOfNumberIsIt(x: number | bigint) {if (typeof x === "bigint") {console.log("'x' is a bigint!");} else {console.log("'x' is a floating-point number");}}
我们要衷心感谢 Caleb Sander 为此功能所做的一切工作。我们非常感谢他的贡献,相信我们的用户也会如此!
注意事项
正如我们提到的,BigInt 支持仅适用于 esnext 目标。可能不那么明显,由于 BigInt 对于数学运算符(如 +、-、* 等)具有不同的行为,因此为该功能不存在的旧版本目标(如 es2017 及以下)提供支持将涉及重写每一个此类操作。TypeScript 需要根据类型分派到正确的行为,因此每次加法、字符串连接、乘法等都将涉及函数调用。
出于这个原因,我们目前没有计划提供降级支持。好消息是,Node 11 和较新版本的 Chrome 已经支持此功能,因此当以 esnext 为目标时,你可以在这些环境中使用 BigInt。
某些目标可能包含 polyfill 或类似 BigInt 的运行时对象。对于这些目的,你可能需要在编译器选项的 lib 设置中添加 esnext.bigint。
非单元类型作为联合判别式
TypeScript 3.2 通过放宽被视为判别属性的规则,使类型收窄变得更容易。只要联合类型的公共属性包含*某个*单例类型(例如字符串字面量、null 或 undefined),并且不包含泛型,它们现在就被视为判别式。
因此,TypeScript 3.2 将以下示例中的 error 属性视为判别式,而以前则不会,因为 Error 不是单例类型。得益于此,类型收窄在 unwrap 函数的主体中可以正常工作。
tstype Result<T> = { error: Error; data: null } | { error: null; data: T };function unwrap<T>(result: Result<T>) {if (result.error) {// Here 'error' is non-nullthrow result.error;}// Now 'data' is non-nullreturn result.data;}
通过 Node.js 包进行 tsconfig.json 继承
TypeScript 3.2 现在可以解析来自 node_modules 的 tsconfig.json。当在 tsconfig.json 的 extends 字段中使用基础路径时,TypeScript 将为我们深入检索 node_modules 包。
{"": "@my-team/tsconfig-base","": ["./**/*"],"": {// Override certain options on a project-by-project basis."": false}}
在这里,TypeScript 将向上遍历 node_modules 文件夹以查找 @my-team/tsconfig-base 包。对于每个包,TypeScript 将首先检查 package.json 是否包含 "tsconfig" 字段,如果有,TypeScript 将尝试从该字段加载配置文件。如果两者都不存在,TypeScript 将尝试读取根目录下的 tsconfig.json。这类似于 Node 用于查找包中 .js 文件的过程,以及 TypeScript 已经使用的 .d.ts 查找过程。
此功能对于大型组织或具有大量分布式依赖的项目非常有用。
新的 --showConfig 标志
TypeScript 编译器 tsc 支持一个新的标志 --showConfig。当运行 tsc --showConfig 时,TypeScript 将计算出有效的 tsconfig.json(在计算完从 extends 字段继承的选项后)并将其打印出来。这对于诊断一般配置问题非常有用。
JavaScript 中的 Object.defineProperty 声明
在 JavaScript 文件(使用 allowJs)中编写时,TypeScript 现在可以识别使用 Object.defineProperty 的声明。这意味着当在 JavaScript 文件中启用类型检查(通过开启 checkJs 选项或在文件顶部添加 // @ts-check 注释)时,你将获得更好的补全提示和更强的类型检查。
js// @ts-checklet obj = {};Object.defineProperty(obj, "x", { value: "hello", writable: false });obj.x.toLowercase();// ~~~~~~~~~~~// error:// Property 'toLowercase' does not exist on type 'string'.// Did you mean 'toLowerCase'?obj.x = "world";// ~// error:// Cannot assign to 'x' because it is a read-only property.