TypeScript 4.1 RC 版本发布:带来了令人兴奋的新特性

2020年11月8日 303点热度 0人点赞 0条评论
图片
作者 | 微软官方博客
译者 | 王强
策划 | 李俊辰
近日,微软正式发布了 TypeScript 4.1 的发布候选(RC)版本。

需要安装这个 RC 版的同学,可以通过 NuGet 获取:

https://www.nuget.org/packages/Microsoft.TypeScript.MSBuild

或使用 npm 命令:
npm install typescript@rc

你还可以通过以下方式获得编辑器支持:

  • 下载 Visual Studio 2019/2017

  • 按 Visual Studio Code 和 Sublime Text 的指南操作。

在这个版本中我们提供了一些令人兴奋的新特性、新的检查标志、编辑器生产力更新和性能改进。

下面就来看看 4.1 为我们准备了哪些内容!

  • 引入字符串模板类型

  • 在映射类型中加入键重映射

  • 递归条件类型

  • 新增检查索引访问功能 --noUncheckedIndexedAccess

  • 使用 path 启用路径映射时可以不指 baseUrl

  • checkJs 现在默认意味着 allowJs,不再需要同时设置 checkJs 和 allowJs

  • 支持 React 17 的 JSX 功能

  • JSDoc @see 标签的编辑器支持

  • 重大更改

Template Literal 类型
我们可以使用 TypeScript 中的字符串字面量类型,来建模需要一组特定字符串的函数和 API。
function setVerticalAlignment(color: "top" | "middle" | "bottom") {
    // ...
}
setVerticalAlignment("middel");
// ~~~~~~~~
// error: Argument of type '"middel"' is not assignable to
// parameter of type '"top" | "middle" | "bottom"'.

这个特性很好用,因为字符串字面量类型可以对我们的字符串值进行基本的拼写检查。

另一个好处是,字符串字面量可以用作映射类型中的属性名称。从这个意义上讲,它们也可用作构建块。
type Options = {
    [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

字符串字面量类型还可以用作另一种构建块:构建其他字符串字面量类型。

所以 TypeScript 4.1 引入了模板字面量字符串类型。它的语法和 JavaScript 中的模板字面量字符串是一样的,只是用在类型的场景中。当它用于字面量的具体类型(concrete type)时,它会串联内容来生成一个新的字符串字面量类型。
type World = "world";
type Greeting = `hello ${World}`;
// same as
// type Greeting = "hello world";
在替代位置有联合类型呢?它会生成可以由每个联合成员表示的所有可能的字符串字面量的集合。
type Color = "red" | "blue";
type Quantity = "one" | "two";
type SeussFish = `${Quantity | Color} fish`;
// same as
// type SeussFish = "one fish" | "two fish"
// | "red fish" | "blue fish";5
这个特性的用途远不止发行说明里的这点小例子。例如,几个用于 UI 组件的库有一种在其 API 中同时指定垂直和水平对齐方式的方法,一般是用两个分别表示横纵轴对齐的字符串连接,例如“bottom- right”。垂直对齐可选的有“top”“middle”和“bottom”,水平对齐有“left”“center”和“right”,加起来有 9 个字符串选项,前后字符串之间都用破折号连接。
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
// Takes
// | "top-left"    | "top-center"    | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;
setAlignment("top-left"); // works!
setAlignment("top-middel"); // error!
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle

虽然这类 API 可用的有很多,但我们可以手动把这些选项都写出来,所以这个例子还是偏玩具一些的。实际上,如果只有 9 个字符串可选那没什么大不了。但当你需要大量字符串时,应考虑提前自动生成它们,这样就用不着那么多类型检查了(或只使用 string,这更容易理解)。

这个特性的一个很有价值的用途是自动态创建新的字符串字面量。例如,想象一个 makeWatchedObject API,它接收一个对象并生成一个几乎相同的对象,但加了一个新的 on 方法来检测属性的更改。
let person = makeWatchedObject({
    firstName: "Homer",
    age: 42, // give-or-take
    location: "Springfield",
});
person.on("firstNameChanged", () => {
    console.log(`firstName was changed!`);
});
注意,on 会侦听事件“firstNameChanged”,而不仅仅是“firstName”。我们如何对其类型化呢?
type PropEventSource<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
这样,当我们赋予错误的属性时,构建出的东西就会报错!
// error!
person.on("firstName", () => {
});
// error!
person.on("frstNameChanged", () => {
});
我们还可以在模板字面量类型里做一些特殊的事情:我们可以从替换位置做推断。我们可以把最后一个示例通用化,从 eventName 字符串的各个部分做推断,以找出关联的属性。
type PropEventSource<T> = {
    on<K extends string & keyof T>
        (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
let person = makeWatchedObject({
    firstName: "Homer",
    age: 42,
    location: "Springfield",
}
);
// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
    // 'newName' has the type of 'firstName'
    console.log(`new name is ${newName.toUpperCase()}`);
}
);
// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
    if (newAge < 0) {
        console.log("warning! negative age");
    }
}
)

在这里我们把 on 变成了一种通用方法。当用户使用字符串“firstNameChanged”进行调用时,TypeScript 会尝试推断 K 的正确类型。为此,它将 K 与“Changed”之前的内容进行匹配,并推断字符串“firstName”。当 TypeScript 推断出来后,on 方法可以获取原始对象上的 firstName 类型,在这里是 string。类似地,当我们使用“ageChanged”调用时,它会找到属性 age 的类型(即 number)。

推断可以有多种组合方式,通常是解构字符串,并以多种方式对其进行重构。实际上,为了帮助大家修改这些字符串字面量类型,我们添加了一些新的实用程序类型别名,用于修改字母中的大小写(也就是转换为小写和大写字符)。
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
type HELLO = EnthusiasticGreeting<"hello">;
// same as
// type HELLO = "HELLO";

新的类型别名为 Uppercase、Lowercase、Capitalize 和 Uncapitalize。前两个会转换字符串中的每个字符,后两个仅转换字符串中的第一个字符。

欲了解更多信息,请参见原始的拉取请求和进行中的拉取请求

https://github.com/microsoft/TypeScript/pull/40336

https://github.com/microsoft/TypeScript/pull/40580

映射类型中加入键重映射
就像刷新器一样,映射类型可以基于任意键创建新的对象类型:
type Options = {
    [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };
或基于其他对象类型创建新的对象类型:
/// 'Partial<T>' is the same as 'T', but with each property marked optional.
type Partial<T> = {
    [K in keyof T]?: T[K]
};

以前,映射类型只能使用你提供的键来生成新的对象类型。但很多时候你希望能够根据输入来创建新键或过滤掉键。

因此,TypeScript 4.1 允许你使用新的 as 子句重新映射映射类型中的键。
type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
    // ^^^^^^^^^^^^^
    // This is the new syntax!
}
有了这个新的 as 子句,你可以利用模板字面量类型之类的特性,轻松地基于旧名称创建属性名称。
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
    name: string;
    age: number;
    location: string;
}
type LazyPerson = Getters<Person>;
你甚至可以生成 never 来过滤掉密钥。这意味着在某些情况下,你不必使用额外的 Omit 帮助程序类型。
// Remove the 'kind' property
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
    kind: "circle";
    radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// same as
// type KindlessCircle = {
// radius: number;
// };

欲了解更多信息,请查看 GitHub 上的原始拉取请求:

https://github.com/microsoft/TypeScript/pull/40336

递归条件类型

在 JavaScript 中,经常能看到可以展开(flatten)并建立任意级别容器类型的函数。例如,考虑 Promise 实例上的.then() 方法。.then(...) 一个个展开 promise,直到它找到一个“不像 promise”的值,然后将该值传递给一个回调。Arrays 上还有一个相对较新的 flat 方法,从中可以看出展开的深度能有多大。

以前,处于各种实际因素,在 TypeScript 的类型系统中无法表达这一点。尽管有一些破解方法可以实现它,但最后出来的类型看起来会很奇怪。

所以 TypeScript 4.1 放宽了对条件类型的一些限制——以便它们可以构建这些模式。在 TypeScript 4.1 中,条件类型现在可以立即在其分支中引用自身,这样我们就更容易编写递归类型别名了。

例如,如果我们想编写一个类型来获取嵌套数组的元素类型,则可以编写以下 deepFlatten 类型。
type ElementType<T> =
    T extends ReadonlyArray<infer U> ? ElementType<U> : T;
function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
    throw "not implemented";
}
// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);
类似地,在 TypeScript 4.1 中,我们可以编写一个 Awaited 类型来深度展开 Promise。
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
    p: Promise<T>,
    onFulfilled: (value: Awaited<T>) => U
)
: Promise<Awaited<U>>
;

请记住,尽管这些递归类型都很强大,但使用它们的时候应该小心谨慎。

首先,这些类型可以完成很多工作,这意味着它们会增加类型检查时间。用它计算 Collatz 猜想或斐波那契数列中的数字可能很有意思,但不要放在 npm 的.d.ts 文件里。

除了计算量大之外,这些类型还可能在足够复杂的输入上触及内部递归深度上限。达到这一递归上限时将导致编译时错误。一般来说最好不要使用这些类型,避免写出一些在更实际的场景中会失败的代码。

实现细节见此:

https://github.com/microsoft/TypeScript/pull/40002

Checked Indexed Accesses
TypeScript 有一个称为索引签名的特性。这些签名可以用来告知类型系统,用户可以访问任意命名的属性。
interface Options {
    path: string;
    permissions: number;
    // Extra properties are caught by this index signature.
    [propName: string]: string | number;
}
function checkOptions(opts: Options) {
    opts.path // string
    opts.permissions // number
    // These are all allowed too!
    // They have the type 'string | number'.
    opts.yadda.toString();
    opts["foo bar baz"].toString();
    opts[Math.random()].toString();
}

在上面的示例中,Options 有一个索引签名,其含义是任何尚未列出的 accessed 属性都应具有 string | number 类型。理想情况下(代码假定你知道自己在干什么)这很方便,但事实是,JavaScript 中的大多数值并不能完整支持所有潜在的属性名称。例如,大多数类型都不会像前面的示例那样,有一个 Math.random() 创建的属性键的值。对于许多用户而言,这种行为是超乎预料的,并且会感觉它没有充分利用 --strictNullChecks 的严格检查。

因此,TypeScript 4.1 加入了一个名为 --noUncheckedIndexedAccess 的新标志。在这种新模式下,每个属性访问(如 foo.bar)或索引访问(如 foo["bar"])都被认为可能是 undefined 的。这意味着在我们的最后一个示例中,opts.yadda 的类型为 string | number | undefined,而不只是 string | number。如果你需要访问该属性,则必须先检查其是否存在,或者使用非 null 断言运算符(后缀 ! 字符)。
// Checking if it's really there first.
if (opts.yadda) {
    console.log(opts.yadda.toString());
}

// Basically saying "trust me I know what I'm doing"
// with the '!' non-null assertion operator.
opts.yadda!.toString();
使用 --noUncheckedIndexedAccess 的一个后果是,即使在边界检查循环中,也会更严格地检查对数组的索引。
function screamLines(strs: string[]) {
    // this will have issues
    for (let i = 0; i < strs.length; i++) {
        console.log(strs[i].toUpperCase());
        // ~~~~~~~
        // error! Object is possibly 'undefined'.
    }
}
如果不需要索引,则可以使用 for–of 循环或 forEach 调用来遍历各个元素。
function screamLines(strs: string[]) {
    // this works fine
    for (const str of strs) {
        console.log(str.toUpperCase());
    }
    // this works fine
    strs.forEach(str => {
        console.log(str.toUpperCase());
    });
}

捕获越界错误时这个标志可能很方便,但它对于很多代码来说可能显得很累赘,因此 --strict 标志不会自动启用它。但如果你对这个特性很感兴趣,也可以随意尝试它,看它是否适合你团队的代码库!

欲了解更多信息,请查看实现的拉取请求:

https://github.com/microsoft/TypeScript/pull/39560

没有 baseUrl 的 paths

路径映射是相当常用的,通常是为了更好地导入,或者为了模拟 monorepo 链接行为。

不幸的是,指定 paths 来启用路径映射时,还需要指定一个名为 baseUrl 的选项,该选项也允许到达相对于 baseUrl 的 bare specifier paths。它还经常会使自动导入使用较差的路径。

在 TypeScript 4.1 中,可以在没有 baseUrl 的情况下使用 path 选项,从而避免其中一些问题。

checkJs 隐含 allowJs

以前,如果你要启动一个 checked 的 JavaScript 项目,则必须同时设置 allowJs 和 checkJs。这有点烦人,因此现在 checkJs 默认隐含了 allowJs。

欲了解更多信息,请查看拉取请求:

https://github.com/microsoft/TypeScript/pull/40275

React 17 JSX 工厂

TypeScript 4.1 通过 jsx 编译器选项的两个新选项,支持了 React 17 即将推出的 jsx 和 jsxs 工厂函数:

  • react-jsx

  • react-jsxdev

这些选项分别用于生产和开发编译环境。一般来说,一个选项可以从另一个扩展而来。例如,用于生产构建的 tsconfig.json 可能如下所示:
// ./src/tsconfig.json
{
    "compilerOptions": {
        "module": "esnext",
        "target": "es2015",
        "jsx": "react-jsx",
        "strict": true
    },
    "include": [
        "./**/*"
    ]
}
用于开发的构建可能如下所示:
// ./src/tsconfig.dev.json
{
    "extends": "./tsconfig.json",
    "compilerOptions": {
        "jsx": "react-jsxdev"
    }
}

欲了解更多信息,请查看相应的 PR:

https://github.com/microsoft/TypeScript/pull/39199

JSDoc @see 标签的编辑器支持
JSDoc @see 标签现在在 TypeScript 和 JavaScript 的编辑器中得到了更好的支持。这样你就可以在标签后的虚线名称中使用 go-to-definition 之类的功能。例如,在下面的示例中,仅对 JSDoc 注释中的 first 或 C 进行 go-to-defintion 即可:
// @filename: first.ts
export class C { }
// @filename: main.ts
import * as first from './first';
/**
 * @see first.C
 */

function related() { }

感谢积极贡献者 Wenlu Wang 实现它:

https://github.com/microsoft/TypeScript/pull/39760

重大更改
abstract 成员不能被标记为 async

标记为 abstract 的成员不能再标记为 async。此处的解决方法是移除 async 关键字,因为调用方只关心返回类型。

any/unknown 在 falsy 位置传播

以前,对于像 foo && somethingElse 这样的表达式,foo 的类型是 any 或 unknown 的,整个表达式的类型将是 somethingElse 的类型。

例如,以前在下列代码中 x 的类型为{ someProp: string }。
declare let foo: unknown;
declare let somethingElse: { someProp: string };
let x = foo && somethingElse;

但在 TypeScript 4.1 中,我们会更谨慎地确定这种类型。由于对 && 左侧的类型一无所知,因此我们将向外传播 any 和 unknown,而不是将右侧的类型传播出去。

它最常见的使用模式出现在检查 booleans 的兼容性时,尤其是在谓词函数中。
function isThing(x: any): boolean {
    return x && typeof x === 'object' && x.blah === 'foo';
}

一般来说,合适的解决方法是从 foo && someExpression 切换到!!foo && someExpression。

条件 spread 创建可选属性

在 JavaScript 中,对象 spread(例如{ ...foo })不会对虚假值起作用。因此,在类似{ ...foo }的代码中,如果 foo 为 null 或 undefined,则会跳过 foo。

许多用户利用此优势“有条件地”在属性中 spread。
interface Person {
    name: string;
    age: number;
    location: string;
}
interface Animal {
    name: string;
    owner: Person;
}
function copyOwner(pet?: Animal) {
    return {
        ...(pet && pet.owner),
        otherStuff: 123
    }
}
// We could also use optional chaining here:
function copyOwner(pet?: Animal) {
    return {
        ...(pet?.owner),
        otherStuff: 123
    }
}

在这里,如果定义了 pet,则 pet.owner 的属性将被 spread 进去;否则,不会将任何属性 spread 到返回的对象中。

copyOwner 的返回类型以前是基于每个 spread 的联合类型:
{ x: number } | { x: number, name: string, age: number, location: string }

这个操作是这样的:如果定义了 pet,Person 的所有属性都将存在;否则,所有属性都不会在结果上定义。要么全有,要么都没有。但有人把这种模式用得太过分了,在单个对象中塞几百个 spread,每个 spread 都可能添加数百或数千个属性。事实证明,由于各种原因,这种做法的成本最后会飞天,并且往往不会带来太多收益。

在 TypeScript 4.1 中,返回的类型改为使用 all-optional 属性。
{
    x: number;
    name?: string;
    age?: number;
    location?: string;
}

这样性能和代码简洁程度都会上一个台阶。

欲了解更多信息,请参见原始更改:

https://github.com/microsoft/TypeScript/pull/40778

--declaration 和 --outFile 需要包名称根
当你有一个同时使用 outFile 和 declaration,来为你的项目发出单个.js 文件以及相应的.d.ts 文件的项目时,该声明文件通常需要对模块标识符进行某种后处理,才能对外部消费者有意义。例如,像这样的项目:
// @filename: projectRoot/index.ts
export * from "./nested/base";
// @filename: projectRoot/nested/base.ts
export const a = "123"
将生成一个如下所示的.d.ts 文件:
declare module "nested/base" {
    export const a = "123";
}
declare module "index" {
    export * from "nested/base";
}
从技术上讲这是准确的,但没那么有用。当请求生成单个.d.ts 文件时,TypeScript 4.1 会要求指定 bundledPackageName。
declare module "hello/nested/base" {
    export const a = "123";
}
declare module "hello" {
    export * from "hello/nested/base";
}
没有这个选项的话,你可能会收到像下面这样的错误消息:
The `bundledPackageName` option must be provided when using outFile and node module resolution with declaration emit.
在 Promise 中,resolve 的参数不再可选

在编写如下代码时:
new Promise(resolve => {
    doSomethingAsync(() => {
        doSomething();
        resolve();
    })
})
你可能会收到这样的错误:
resolve()
  ~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
  An argument for 'value' was not provided.
这是因为 resolve 不再具有可选参数,因此默认情况下现在必须为它传递一个值。一般来说,使用 Promise 时这样会捕获合法错误。典型的解决方法是为其传递正确的参数,有时还要添加一个显式的类型参数。
new Promise<number>(resolve => {
    // ^^^^^^^^
    doSomethingAsync(value => {
        doSomething();
        resolve(value);
        // ^^^^^
    })
})
但有时确实需要在没有参数的情况下调用 resolve()。在这些情况下,我们可以给 Promise 一个显式的 void 泛型类型参数(即将其写为 Promise<void>)。这利用了 TypeScript 4.1 中的新功能,其中可能是 void 的尾随参数可以变为可选。
new Promise<void>(resolve => {
    // ^^^^^^
    doSomethingAsync(() => {
        doSomething();
        resolve();
    })
})

TypeScript 4.1 附带了一个快速修复以帮助解决这个问题。

下一步计划

在接下来的几周内,我们将密切注意 TypeScript 4.1 的稳定版本中需要包含的所有高优先级修复。如果可以的话,请试试我们的 RC 版本,帮助我们找出各种潜在问题。我们一直在努力改善大家的 TypeScript 体验!

编程快乐!


延伸阅读

https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-rc/#breaking-changes

图片

45120TypeScript 4.1 RC 版本发布:带来了令人兴奋的新特性

这个人很懒,什么都没留下

文章评论