TypeScript 3.2

strictBindCallApply

TypeScript 3.2引入了一个新的--strictBindCallApply编译选项(是--strict选项家族之一)。在使用了此选项后,函数对象上的bindcallapply方法将应用强类型并进行严格的类型检查。

function foo(a: number, b: string): string {
    return a + b;
}

let a = foo.apply(undefined, [10]);              // error: too few argumnts
let b = foo.apply(undefined, [10, 20]);          // error: 2nd argument is a number
let c = foo.apply(undefined, [10, "hello", 30]); // error: too many arguments
let d = foo.apply(undefined, [10, "hello"]);     // okay! returns a string

它的实现是通过引入了两种新类型来完成的,即lib.d.ts里的CallableFunctionNewableFunction。这些类型包含了针对常规函数和构造函数上bindcallapply的泛型方法声明。这些声明使用了泛型剩余参数来捕获和反射参数列表,使之具有强类型。在--strictBindCallApply模式下,这些声明作用在Function类型声明出现的位置。

警告

由于更严格的检查可能暴露之前没发现的错误,因此这是--strict模式下的一个破坏性改动。

此外,这个新功能还有另一个警告。由于有这些限制,bindcallapply无法为重载的泛型函数或重载的函数进行完整地建模。 当在泛型函数上使用这些方法时,类型参数会被替换为空对象类型({}),并且若在有重载的函数上使用这些方法时,只有最后一个重载会被建模。

对象字面量的泛型展开表达式

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里预定义的PickExclude助手类型,并结合使用泛型类型和解构式里的其它绑定名实现的。

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可以为BigInit进行类型检查,并支持在目标为esnext时输出BigInit字面量。

为支持BigInt,TypeScript引入了一个新的原始类型bigint(全小写)。 可以通过调用BigInt()函数或书写BigInt字面量(在整型数字字面量末尾添加n)来获取bigint

let foo: bigint = BigInt(100); // the BigInt function
let 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)

尽管你可能会认为numberbigint能互换使用,但它们是不同的东西。

declare 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里规定,在算术运算符里混合使用numberbigint是一个错误。 应该显式地将值转换为BigInt

console.log(3.141592 * 10000n);     // error
console.log(3145 * 10n);            // error
console.log(BigInt(3145) * 10n);    // okay!

还有一点要注意的是,对bigint使用typeof操作符返回一个新的字符串:"bigint"。 因此,TypeScript能够正确地使用typeof细化类型。

function 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时才支持。 可能不是很明显的一点是,因为BigInts针对算术运算符+, -, *等具有不同的行为,为老旧版(如es2017及以下)提供此功能时意味着重写出现它们的每一个操作。 TypeScript需根据类型和涉及到的每一处加法,字符串拼接,乘法等产生正确的行为。

因为这个原因,我们不会立即提供向下的支持。 好的一面是,Node 11和较新版本的Chrome已经支持了这个特性,因此你可以在目标为esnext时,使用BigInt。

一些目标可能包含polyfill或类似BigInt的运行时对象。 基于这些考虑,你可能会想要添加esnext.bigintlib编译选项里。

Non-unit types as union discriminants

TypeScript 3.2放宽了作为判别式属性的限制,来让类型细化变得容易。 如果联合类型的共同属性包含了_某些_单体类型(如,字面符字面量,nullundefined)且不包含泛型,那么它就可以做为判别式。

因此,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) {
        // Here 'error' is non-null
        throw result.error;
    }

    // Now 'data' is non-null
    return result.data;
}

tsconfig.json可以通过Node.js包来继承

TypeScript 3.2现在可以从node_modules里解析tsconfig.json。如果tsconfig.json文件里的"extends"设置为空,那么TypeScript会检测node_modules包。 When using a bare path for the "extends" field in tsconfig.json, TypeScript will dive into node_modules packages for us.

{
    "extends": "@my-team/tsconfig-base",
    "include": ["./**/*"]
    "compilerOptions": {
        // Override certain options on a project-by-project basis.
        "strictBindCallApply": false,
    }
}

这里,TypeScript会去node_modules目录里查找@my-team/tsconfig-base包。针对每一个包,TypeScript检查package.json里是否包含"tsconfig"字段,如果是,TypeScript会尝试从那里加载配置文件。如果两者都不存在,TypeScript尝试从根目录读取tsconfig.json。这与Nodejs查找.js文件或TypeScript查找.d.ts文件的已有过程类似。

这个特性对于大型组织或具有很多分布的依赖的工程特别有帮助。

The new --showConfig flag

tsc,TypeScript编译器,支持一个新的标记--showConfig。 运行tsc --showConfig时,TypeScript计算生效的tsconfig.json并打印(继承的配置也会计算在内)。 这对于调试诊断配置问题很有帮助。

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();
//    ~~~~~~~~~~~
//    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.