Typescript 4.0新特性一览|牛气冲天新年征文

690 阅读7分钟

可变元组类型(Variadic Tuple Types)

旧版本Typescript要实现一个concat方法,一般实现方式是:

function concat<T, U>(arr1: T[], arr2: U[]) {
  return [...arr1, ...arr2]
}

这种方式实现的concat方法的返回值类型是一个联合类型的数组(T | U)[]

declare const arr1: number[]
declare const arr2: string[]

// type: (string | number)[]
const arr = concat(arr1, arr2)

如果要对元祖进行concat操作呢

declare const arr1: [number, string]
declare const arr2: [boolean]

// type: (string | number | boolean)[]
const arr = concat(arr1, arr2)

元祖具有特定长度和元素类型,上例的返回值类型显然是不精确的,期望的返回值类型是[number, string, boolean]

要实现期望的结果,在旧版本的TS中只能编写重载

function concat<T, U, V>(arr1: [T, U], arr2: [V]): [T, U, V]
function concat<T, U>(arr1: T[], arr2: U[]) {
  return [...arr1, ...arr2]
}

但如果传入的元祖长度不能确定,我们只能不断的编写重载以尽可能覆盖所有的情况,这显然是不可接受的。

TypeScript 4.0 带来了两个基础更改,并在推断方面进行了改进。

其中一个更改是范型可用于扩展运算符。这意味着可以用范型声明一个可变的元祖。

由此就可以实现一个类型支持更好的concat函数

function concat<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]): [...T, ...U] {
    return [...t, ...u];
}

declare const arr1: [string, number]
declare const arr2: string[]
declare const arr3: ['hello']

concat(arr1, arr2) // [string, number, ...string[]]
concat(arr1, arr3) // [string, number, 'hello']

另一个更改是旧版本Typescript的rest参数只支持数组类型,且必须放在元祖的最后;而现在可以放在元祖的任意位置。

// 旧版本ts
type t1 = [...string[]]
type t2 = [...[string, number]] // error: A rest element type must be an array type.
type t3 = [...string[], string] // A rest element must be last in a tuple type

不确定长度的数组类型使用扩展运算符,如果不放置于最后,那其后的所有元素都将被推断该数组元素的类型和其后元素类型的联合类型

type t1 = string[]
type t2 = [boolean, ...t1, number] // [boolean, ...(string | number)[]]

标记元祖元素(Labeled Tuple Elements)

如果我们要创建一个图标,可能有如下实现

function createIcon(url: string, size: [number, number]) {
    const [width, height] = size
    return new BMapGL.Icon(url, new BMapGL.Size(width, height));
}

其中的size参数是元祖类型,但我们在调用createIcon方法的时候只清楚size的类型,并不清楚每个元素的意义。在调用时还需要跳转到函数体去查看size每个元素的意义。

在Typescript 4.0中,元祖元素可以被标记。上例中的createIcon方法可以这样实现:

function createIcon(url: string, size: [width: number, height: number]) {
    const [width, height] = size
    return new BMapGL.Icon(url, new BMapGL.Size(width, height));
}

这样在调用时就能查看size参数每个元素的意义

一些使用规则

在标记一个元组元素时,还必须标记元组中的所有其他元素。

type Size = [width: number, number] // error: Tuple members must all have names or all not have names.

解构标记时无需使用不同名称命名变量。如下例,解构size参数时变量命名无需使用widthheight

function createIcon(url: string, size: [width: number, height: number]) {
    const [w, h] = size
    return new BMapGL.Icon(url, new BMapGL.Size(w, h));
}

使用带标记的元祖可实现重载

从构造器函数中推断类属性(Class Property Inference from Constructors)

如下例,在旧版本的Typescript中,当开启了noImplicitAny选项,定义的实例属性areasideLength会报错。因为其没有显式声明类型,从而被推断为any。

class Square { 
    area; 
    sideLength; 
    constructor(sideLength: number) { 
        this.sideLength = sideLength; 
        this.area = sideLength ** 2; 
    } 
}

而在Typescript 4.0中该实例属性的类型会从 constructor函数中推断,areasideLength都被推断为number类型,不会报错。

如果对类实例属性的初始化没有写在constructor函数中,Typescript就无法推断该实例属性的类型。

class Square { 
    // error: Member 'sideLength' implicitly has an 'any' type.
    sideLength; 
    constructor(sideLength: number) { 
        this.initialize(sideLength) 
    } 
    initialize(sideLength: number) { 
        this.sideLength = sideLength; 
    }
} 

此时需要显示声明实例属性的类型,而如果开启strictPropertyInitialization选项(检查已声明但未在构造函数中设置的类属性)还需要显示赋值断言来使类型系统识别类型

class Square {
    sideLength!: number; 
    constructor(sideLength: number) { 
        this.initialize(sideLength) 
    } 
    initialize(sideLength: number) { 
        this.sideLength = sideLength; 
    } 
} 

短路赋值运算符(Short-Circuiting Assignment Operators)

ES2021新增的特性中包含了逻辑赋值运算符(Logical Assignment Operators)的提案。

当变量a为truthy时,将其值设置为b,即等价于a = a && b

a &&= b;

当变量a为falsy时,将其设置为b,即等价于a = a || b

a ||= b;

当变量a为nullish时,将其设置为b,即等价于a = a ?? b

??操作符是ES2020的新增特性Nullish Coalescing

// set a to b only when a is nullish
a ??= b;

Typescript 4.0支持了上述特性

catch子句变量支持声明为unknown(unknown on catch Clause Bindings)

在旧版本的Typescript中,catch子句的变量拥有any类型,且不可以被声明为其他类型

try {

} catch(err: unknown) { // error: Catch clause variable cannot have a type annotation.

}

在Typescript 4.0版本支持将该变量声明为unknown,上例不会报错。

之所以这样做是因为any类型可以兼容其他所有类型,如上例对err变量进行任何操作都不会报类型错误。而unknownany 更安全,因为它会在我们操作值之前提醒我们执行某种类型检查。

try {

} catch(err: unknown) {
    if (err instanceof Error) {
        console.error(err.message)
    }
}

定制 JSX Fragment 工厂函数

旧版本的Typescript便已支持定制JSX工厂函数,可通过jsxFactory选项进行定制。

在Typescript 4.0中支持通过新的jsxFragmentFactory 选项来定制 Fragment 工厂函数。

如下tsconfig.json配置告诉 TypeScript 以与 React 兼容的方式转换 JSX,但将每个工厂函数切换为 h 而不是 React.createElement,并使用 Fragment 而不是 React.Fragment

使用如下tsconfig.json配置

{ 
  "compilerOptions": { 
    "target": "esnext", 
    "module": "commonjs", 
    "jsx": "react", 
    "jsxFactory": "h", 
    "jsxFragmentFactory": "Fragment" 
  } 
} 

编译如下代码

import { h, Fragment } from "preact"; 
let stuff = <> 
    <div>Hello</div> 
</>; 

将输出

"use strict";
exports.__esModule = true;
/** @jsx h */
/** @jsxFrag Fragment */
var preact_1 = require("preact");
var stuff = preact_1.h(preact_1.Fragment, null,
    preact_1.h("div", null, "Hello"));

JSX工厂函数支持使用/** @jsx */注释,去指定当前文件使用的JSX工厂函数。同样,Fragment工厂函数可通过新的/** @jsxFrag */注释去指定。

如下,在文件头部指定当前文件使用的JSX工厂函数和Fragment工厂函数。

通过注释指定的方式比在tsconfig.json文件中配置的优先级高

/** @jsx h */ 
/** @jsxFrag Fragment */ 
import { h, Fragment } from "preact"; 
let stuff = <> 
    <div>Hello</div> 
</>; 

重大更改

lib.d.ts

Typescript 4.0删除了 document.origin,它仅在 IE 的旧版本中有效,而 Safari MDN 建议改用 self.origin。

如下,在Typescript 4.0版本访问documentorigin属性将提示该属性不存在

document.origin // error: Property 'origin' does not exist on type 'Document'

如果要在旧版本的IE使用该属性,需要显式设置

interface Document {
    origin: string
}

console.log(document.origin)

属性重写访问器(反之亦然)会报错(Properties Overriding Accessors (and vice versa) is an Error)

旧版本的Typescript中,子类的实例属性覆盖父类的访问器属性只有在使用useDefineForClassFields选项时才会报错。

class Base { 
    get foo() { 
        return 100; 
    } 
    set foo(val) { 
        // ... 
    } 
} 
class Derived extends Base {
// 旧版本在使用useDefineForClassFields选项会报错 error: 'foo' is defined as an accessor in class 'Base', but is overridden here in 'Derived' as an instance property.
    foo = 10;
} 

而在Typescript 4.0版本中,无论是否使用useDefineForClassFields选项,子类的实例属性覆盖父类的访问器属性(或子类的访问器属性覆盖父类的实例属性)总是报错。

delete 的操作对象必须是可选的

Typescript 4.0版本在启用 strictNullChecks 选项时,使用delete 运算符,操作对象现在必须为 anyunknownnever 或为可选(因为它在类型中包含 undefined)。否则,使用 delete 运算符将会报错。

interface Thing { 
    prop: string; 
    a: unknown;
    b: any;
    c: never;
    d: undefined;
} 
function f(x: Thing) { 
    delete x.prop; // error: The operand of a 'delete' operator must be optional.
    delete x.a
    delete x.b
    delete x.c
    delete x.d
}

参考资料