TypeScript 新特性拾遗

好久没有写博客了,也由于工作变动的原因,好久没有关注 TypeScript 的 Release Note 了, 今天趁着项目空档摸鱼的间隙好好来学习下。

类型谓词 (type predicates)

该特性存在已久,但在 5.5 版本中,得到了增强,在特定的情况下可以省略类型谓词而达到同样的效果。

// 以前需要这么写:
const isNumber = (x: unknown): x is number => typeof x === 'number';

// 现在可以省略类型谓词
const isNumber = (x: unknown) => typeof x === 'number';

Playground

当然,自动推断类型谓词需要符合如下条件:

  1. The function does not have an explicit return type or type predicate annotation.
  2. The function has a single return statement and no implicit returns.
  3. The function does not mutate its parameter.
  4. The function returns a boolean expression that’s tied to a refinement on the parameter.

如下是一个不能自动推断的反例:

[0, 1, 2, 3, null, 5].filter(x => !!x).map(i => i.toFixed());
// 'i' is possibly 'null'.

违背了上述的第二个条件:

TypeScript did not infer a type predicate for x => !!x, and rightly so: if this returns true then x is a number. But if it returns false, then x could be either undefined or a number (specifically, 0). This is a real bug: if any student got a zero on the test, then filtering out their score will skew the average upwards. Fewer will be above average and more will be sad!

闭包与类型缩窄

在之前的版本中,类型缩窄后,在作用域内的闭包函数内是不生效的,看例子:

function getUrls(url: string | URL, names: string[]) {
    if (typeof url === "string") {
        url = new URL(url);
    }

    return names.map(name => {
        url.searchParams.set("name", name)
        //  ~~~~~~~~~~~~
        // error!
        // Property 'searchParams' does not exist on type 'string | URL'.
        return url.toString();
    });
}

这在早期是可以理解的,因为回调函数的调用时机不可预测,这就导致哪怕前面已经通过 if 判断缩窄了 url 的类型到 URL,但无法保证在回调函数被调用时,其它地方不会将 url修改为 string。

在 5.4 版本中,如果在作用域范围内,没有对 url 重新赋值其它类型,则认为在回调的闭包内依旧是缩窄后的类型:

function getUrls(url: string | URL, names: string[]) {
    if (typeof url === "string") {
        url = new URL(url);
    }
    // url = 'xxx';  将会导致类型缩窄失效
    // setTimeout(() => url = 'xxx'); 也会导致类型缩窄失效
    return names.map(name => {
        // 否则,url 就是 URL 类型
        url.searchParams.set("name", name)
        return url.toString();
    });
}

NoInfer

Release Note 中的例子很贴切:

function createStreetLight<C extends string>(colors: C[], defaultColor?: C) {
    // ...
}
// 我们预期是这样: defaultColor 应该是 colors 中的其中一个
createStreetLight(["red", "yellow", "green"], "red");

// 但实际上,这里 C 被推断成了 "red" | "yellow" | "green" | "blue"
createStreetLight(["red", "yellow", "green"], "blue");

// 为了处理这种情况,一般会新增第二个泛型参数来约束 defaultColor
function createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) {
}

createStreetLight(["red", "yellow", "green"], "blue");
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.

在 5.4 之后,新增了 NoInfer Utility type 后就不需要第二个泛型参数了,可以直接这样:

function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) {
    // ...
}

createStreetLight(["red", "yellow", "green"], "blue");
//                                            ~~~~~~
// error!
// Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'.