TypeScript 3.2
strictBindCallApply
TypeScript 3.2 引入了一个新的 strictBindCallApply 编译器选项(属于 strict 系列选项),通过该选项,函数对象上的 bind、call 和 apply 方法将具有强类型并进行严格检查。
function foo(a: number, b: string): string {
return a + b;
}
let a = foo.apply(undefined, [10]); // 错误:参数过少
let b = foo.apply(undefined, [10, 20]); // 错误:第二个参数是数字
let c = foo.apply(undefined, [10, "hello", 30]); // 错误:参数过多
let d = foo.apply(undefined, [10, "hello"]); // 正确!返回字符串这是通过在 lib.d.ts 中引入两个新类型 CallableFunction 和 NewableFunction 实现的。这些类型分别包含针对普通函数和构造函数上的 bind、call 和 apply 的专用泛型方法声明。这些声明使用泛型剩余参数(参见 #24897)以强类型方式捕获和反映参数列表。在 strictBindCallApply 模式下,这些声明将取代由 Function 类型提供的(非常宽松的)声明。
注意事项
由于更严格的检查可能发现以前未报告的错误,因此在 strict 模式下这是一个破坏性更改。
此外,此新功能的另一个注意事项是,由于某些限制,bind、call 和 apply 尚不能完全模拟泛型函数或具有重载的函数。在泛型函数上使用这些方法时,类型参数将被替换为空对象类型({}),而在具有重载的函数上使用时,只有最后一个重载会被模拟。
对象字面量中的泛型展开表达式
在 TypeScript 3.2 中,对象字面量现在允许使用泛型展开表达式,这些表达式会产生交叉类型,类似于 Object.assign 函数和 JSX 字面量。例如:
function 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" }属性赋值和非泛型展开表达式会在泛型展开表达式的两侧尽可能地合并。例如:
function 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 }
}非泛型展开表达式继续按以前的方式处理:调用和构造签名被剥离,仅保留非方法属性,对于同名的属性,使用最右侧属性的类型。这与交叉类型形成对比,交叉类型会连接调用和构造签名、保留所有属性、并交叉同名属性的类型。因此,相同类型的展开在通过泛型类型实例化时可能产生不同的结果:
function 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; // string
let b2 = s2.b; // number & string泛型对象剩余变量和参数
TypeScript 3.2 还允许从泛型变量中解构剩余绑定。这是通过使用 lib.d.ts 中预定义的 Pick 和 Exclude 辅助类型,并在解构模式中使用相关泛型类型以及其他绑定的名称来实现的。
function 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:
let foo: bigint = BigInt(100); // BigInt 函数
let bar: bigint = 100n; // BigInt 字面量
// *拍拍斐波那契函数的车顶*
// 这个家伙返回的整数可以变得*如此*大!
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 之间有密切的交互,但两者是独立的域。
declare let foo: number;
declare let bar: bigint;
foo = bar; // 错误:类型 'bigint' 不能赋值给类型 'number'。
bar = foo; // 错误:类型 'number' 不能赋值给类型 'bigint'。根据 ECMAScript 规范,在算术运算中混合 number 和 bigint 是错误的。你必须显式地将值转换为 BigInt。
console.log(3.141592 * 10000n); // 错误
console.log(3145 * 10n); // 错误
console.log(BigInt(3145) * 10n); // 正确!同样重要的是要注意,使用 typeof 运算符时,bigint 会生成一个新字符串:字符串 "bigint"。因此,TypeScript 会像你期望的那样使用 typeof 正确进行收窄。
function whatKindOfNumberIsIt(x: number | bigint) {
if (typeof x === "bigint") {
console.log("'x' 是一个 bigint!");
} else {
console.log("'x' 是一个浮点数");
}
}我们要特别感谢 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 函数体内的收窄正确工作。
type Result<T> = { error: Error; data: null } | { error: null; data: T };
function unwrap<T>(result: Result<T>) {
if (result.error) {
// 此处 'error' 非空
throw result.error;
}
// 现在 'data' 非空
return result.data;
}通过 Node.js 包继承 tsconfig.json
TypeScript 3.2 现在从 node_modules 解析 tsconfig.json。当在 tsconfig.json 中使用裸路径作为 extends 字段时,TypeScript 将为我们深入 node_modules 包中查找。
{
"extends": "@my-team/tsconfig-base",
"include": ["./**/*"],
"compilerOptions": {
// 按项目覆盖某些选项。
"strictBindCallApply": 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 注释),你将获得更好的补全和更强的类型检查。
// @ts-check
let obj = {};
Object.defineProperty(obj, "x", { value: "hello", writable: false });
obj.x.toLowercase();
// ~~~~~~~~~~~
// 错误:
// 属性 'toLowercase' 在类型 'string' 上不存在。
// 你是想用 'toLowerCase' 吗?
obj.x = "world";
// ~
// 错误:
// 不能赋值给 'x',因为它是只读属性。