likes
comments
collection
share

Typescript 4.4 发布 - 特性中文介绍

作者站长头像
站长
· 阅读数 12

经过beta 和 rc版本,Typescript 4.4 的正式版本在8.26发布了。下面来看看有哪些更新:

流程控制类型推断支持Aliased Conditions和Discriminants

由于javascript是一种弱类型的语言,我们在使用javascript的时候,经常会根据某个变量的类型来做不同的处理。当这样的逻辑用typescript来编写的时候,typescript会识别你条件语句中的类型校验(typescript中称之为type guard 类型哨兵),来进行类型推断。下面是官方release note中的一个例子:

function foo(arg: unknown) {
    if (typeof arg === "string") {
        console.log(arg.toUpperCase());
    }
}

在这个例子里面,虽然arg参数是个unknown类型,但是if语句中使用了javascript的typeof语句来判断参数的类型,这个时候,在这个if语句的block内,typescript就能推断出arg参数的类型是string,自然就可以调用string类型下的toUpperCase方法。

但是,如果我们把if语句中的类型判断结果赋值给到一个变量,会怎么样呢?

function foo(arg: unknown) {
    const argIsString = typeof arg === "string";
    if (argIsString) {
        console.log(arg.toUpperCase());
        //              ~~~~~~~~~~~
        // Error! Property 'toUpperCase' does not exist on type 'unknown'.
    }
}

在4.4版本之前,typescript类型校验就会报错。

在4.4版本,这样的typescript代码是没问题的。typescript发现我们在流程控制中判断一个常量时,会做一些额外的工作去看常量本身在之前有没有包含一些类型校验的逻辑。这样的功能适用于以下场景:对const变量readonly的属性没有被重新赋值的函数参数进行类型校验。

这里我补充一些例子:

class Test {
    readonly name: unknown ='readonlyString'
    writableName: unknown = 'writableNameString'
}

function foo(arg: unknown, arg2: unknown) {
  const argIsString = typeof arg === "string";
  const a = argIsString; // 这里我多赋值一次也没问题
  if (a) {
      console.log(arg.toUpperCase());
  }

  const b = new Test();
  const type = typeof b.name === 'string'
  const wriatbleType =  typeof b.writableName === 'string'

  if (type) {
      console.log(b.name.toUpperCase());
  }

  if (wriatbleType) {
  		// wriatbleType 不是一个readonly的属性
      console.log(b.writableName.toUpperCase());
      //                         ~~~~~~~~~~~
      // Error! Property 'toUpperCase' does not exist on type 'unknown'.
  }
	
  arg2 = arg;
  const arg2IsString = typeof arg2 === "string";
  if (arg2IsString) {
    // 由于arg2 被重新赋值了,所以控制流中的类型推断失效了
    console.log(arg2.toUpperCase());
    //               ~~~~~~~~~~~
    // Error! Property 'toUpperCase' does not exist on type 'unknown'.
  }
}

除了typeof的类型校验条件语句,其他的类型校验也是奏效的。官方文档里面用discriminated unions(这个我不知道怎么翻译)作为例子:

type Shape =
    | { kind: "circle", radius: number }
    | { kind: "square", sideLength: number };

function area(shape: Shape): number {
    const isCircle = shape.kind === "circle";
    if (isCircle) {
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    }
    else {
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}

例子中,Shape 是一个discriminated unions 类型。typescript现在具备在if条件语句中,通过判断shape.kind === 'circle', 推断shape变量属于discriminated unions 类型中具体的类型。所以,在 if(isCircle) 的block中shape可以取得radius属性而不报类型异常,这在之前版本的typescript中是不支持的。

另外在typescript 4.4版本中,对discriminated unions 类型的类型推断,会更深入一点 -- 我们可以把discriminated unions 类型变量的属性提取出来,然后进行条件语句判断,typescript也能正确地进行类型推断。

官方的例子:

type Shape =
    | { kind: "circle", radius: number }
    | { kind: "square", sideLength: number };

function area(shape: Shape): number {
    // Extract out the 'kind' field first.
    const { kind } = shape;

    if (kind === "circle") {
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    }
    else {
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}

其实笔者这里没有理解这里“更深入”的意思,有理解的同学可以发表一下意见,原文是

Analysis on discriminants in 4.4 also goes a little bit deeper

此外,还举了两个例子,说明了类型推断对流程控制的代码做了比较多的升级。笔者看着这两个例子是有点感动。

function doSomeChecks(
    inputA: string | undefined,
    inputB: string | undefined,
    shouldDoExtraWork: boolean,
) {
    let mustDoWork = inputA && inputB && shouldDoExtraWork;
    if (mustDoWork) {
        // We can access 'string' properties on both 'inputA' and 'inputB'!
        const upperA = inputA.toUpperCase();
        const upperB = inputB.toUpperCase();
        // 在以前的版本你需要这么写
        // const upperA = inputA!.toUpperCase();
        // const upperB = inputB!.toUpperCase();
        // 而且如果你有强迫症,你还得把eslint的这个规则关掉
        // @typescript-eslint/no-non-null-assertion
    }
}

这个例子相当于typescript 理解了if语句前面两个&的逻辑,所以在if的block里面,我们不需要借助非空操作符(inputA!. inputB!.)来调用toUpperCase函数。

// 这个例子我做了一下改动,因为boolean类型没什么常用的原型方法
function f(x: string | number | boolean) {
  const isNumber = typeof x === "number";
  const isBoolean = typeof x === "boolean";
  const isBooleanOrNumber = isNumber || isBoolean;
  if (isBooleanOrNumber) {
      x;  // Type of 'x' is 'boolean | number'.
  }
  else {
      x.replace('s', 'a');  // Type of 'x' is 'string'.
    	// 在以前的版本你需要这么写
    	// (x as string).replace('s', 'a');
  }
}

这个例子,typescript成功地推断出了else 分支中,x只能是string类型,所以可以直接调用string的原型方法。如果是以前的typescript 版本,就得上as大法了。

综上述,就是typescript类型系统的类型推断越来越完善,以前我们觉得“这都推断不出来吗?这么蠢”的地方越来越少。

最后,针对discriminated unions 类型的加强,笔者做了下面这个尝试

type PersonInfo =
  | { sex: "male", gameCost: number, cost: { game:number } }
  | { sex: "female", clothesCost: number, cost: { clothes:number } };

function statistics(info: PersonInfo): number {
  const { sex: sexInfo, cost } = info;
  if (sexInfo === 'male') {
      statisticsInfo = cost.game;
    	// Error!!           ~~~~
      // 类型“{ game: number; } | { clothes: number; }”上不存在属性“game”。
  }
  else {
      statisticsInfo = cost.clothes;
    	// Error!!           ~~~~~~~
      // 类型“{ game: number; } | { clothes: number; }”上不存在属性“clothes”。
  }

  const { sex } = info;
  if (sex === 'male') {
      const { cost: costInfo }= info
      statisticsInfo =  costInfo.game;
  }
  else {
      const { cost }= info
      statisticsInfo = cost.clothes;
  }
  return statisticsInfo;
}

发现如果提前将info的cost类型提取出来,那么在条件分支的block中就无法正确地推断类型了。 原因有时间再去implement中找一下(咦,这里为什么有只鸽子Typescript 4.4 发布 - 特性中文介绍

类的静态代码块(static blocks

typescript 支持类的静态代码块(static blocks)的语法。静态代码块语法目前还是一个ECMAScript的提案,目前已经处于stage3,下一阶段就是正式纳入语言规范了。

那静态代码块是什么鬼,笔者相信平常大家也是比较少用到。笔者也是类比着Java的语法进行理解:

  • 它是随着类的加载而执行,只执行一次。具体说,静态代码块是由类调用的。类调用时,先执行静态代码块。
  • 静态代码块其实就是给类初始化的。
  • 静态代码块中的变量是局部变量,与普通函数中的局部变量性质没有区别。
  • 一个类中可以有多个静态代码块

一般在静态代码块中,来执行一些类初始化的代码。在静态代码块中,你可以编写各种语句,甚至是try catch。 下面是一些例子:

class Foo {
    static #count = 0;

    get count() {
        return Foo.#count;
    }

    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

这个是在静态代码块中使用try catch的例子。

let getX: (obj: C) => X;

type X = {data: number}

export class C {
  #x: X
  static #y = 0;
  constructor(x: number) {
    console.log('class C constructor block');
    this.#x = { data: x };
  }

  static gety() {
    return this.#y;
  }

  static {
    // getX has privileged access to #x
    console.log('class C static block');
    getX = (obj) => obj.#x;
  }
}

export function readXData(obj: C) {
  return getX(obj).data;
}

C.gety()
const c = new C(1);
const c2 = new C(2);
console.log(readXData(c));
console.log(readXData(c2));

// 最终的输出顺序:
// class C static block
// class C constructor block
// class C constructor block
// 1
// 2

这个例子是说明一下,静态代码快和构建函数的执行顺序的。

tsc --help 完善

略。

性能优化

略。

嵌入式提示

typescript 4.4 给你的代码展示更多的提示。比如:

  • 函数调用时,提示函数参数的名称

Typescript 4.4 发布 - 特性中文介绍 定义了一个callName函数,函数参数变量名为name。在你调用callName函数时,会提示你当前传入的变量是函数定义时的name参数(上图红色线条画出来的部分)。在我们调用定义很多个参数的函数时,这个提示其实还提好用的。

  • 提示函数的返回类型

Typescript 4.4 发布 - 特性中文介绍 代码中没有定义callName函数的返回值,嵌入式提示帮你提示出来,函数的返回值类型。等于我写了Typescript 4.4 发布 - 特性中文介绍

**注意:**这样的提示需要自己到Vscode 的setting里面去设置。搜索 typescript.inlayHints

Breaking Changes

注意啦!!!

lib.d.ts变更

基操勿6。具体变更在这:lib.d.ts change from 4.3 to 4.4 后面实际项目中如果有遇到一些常用的用法报错了,需要手动更改代码的case,笔者会在这里补充,目前还没发现。 ​

catch语句的error参数类型改为unknown

try catch 语句中的catch后面的error参数的类型由原来的any改成了unknown。如果你之前的项目使用了strict模式,又在catch的block中直接使用了Error的属性时,就会报错了。

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

解决办法1

直接上any大法:

try {
  
} catch (error: any) {
	console.log(error.message)
}

解决办法2

不用strict模式。将tsconfig.json文件中 strict配置为false。 Typescript 4.4 发布 - 特性中文介绍

解决办法3

增加useUnknownInCatchVariables配置 Typescript 4.4 发布 - 特性中文介绍

完善对总是true的promise的检查

在typescript 4.3中,增加了一项特性,对总是返回true的promise的检查。比如:

async function foo(): Promise<boolean> {
    return false;
}

async function bar(): Promise<string> {
    const fooResult = foo();
    if (fooResult) {        // <- error! :D
        return "true";
    }
    return "false";
}

这样的代码,typescript会检查出,当函数返回类型定义为Promise,函数返回值用条件语句判断时,总是为真,typescript认为这样的代码是有问题,并且会提示你是不是忘了用await。 Typescript 4.4 发布 - 特性中文介绍 但是,在4.3版本中,如果你这么写,这样的检查就不奏效了。

async function foo(): Promise<boolean> {
  return false;
}

async function bar(): Promise<string> {
  if (foo()) {        // <- no error
      return "true";
  }
  return "false";
}

所以在4.4版本中,把这个特性更加完善了。

类的抽象属性

在4.4版本中,抽象属性与函数不允许初始化赋值,会报错。

abstract class C {
    abstract prop = 1;
    //       ~~~~
    // Property 'prop' cannot have an initializer because it is marked abstract.
}

这个应该影响不大,因为如果知道使用抽象类、抽象属性、抽象函数的人,应该也不会给他们初始化赋值。

Next. Typescript 4.5

4.5版本的一些特性计划:TypeScript 4.5 Iteration Plan

refs

Announcing TypeScript 4.4

最后

微信搜索公众号Eval Studio,关注更多动态。