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

2020 年 11 月 13 日

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

本月 3 日,微软正式发布了 TypeScript 4.1 的发布候选(RC)版本。

 

需要安装这个 RC 版的同学,可以通过NuGet获取,或使用 npm 命令:

npm install typescript@rc
复制代码

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

 

 

在这个版本中我们提供了一些令人兴奋的新特性、新的检查标志、编辑器生产力更新和性能改进。下面就来看看 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 Seattl
复制代码

虽然这类 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。前两个会转换字符串中的每个字符,后两个仅转换字符串中的第一个字符。

 

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

映射类型中加入键重映射

就像刷新器一样,映射类型可以基于任意键创建新的对象类型:

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' propertytype 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 上的原始拉取请求

递归条件类型

在 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>>;
复制代码

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

 

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

 

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

 

实现细节见此

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 标志不会自动启用它。但如果你对这个特性很感兴趣,也可以随意尝试它,看它是否适合你团队的代码库!

 

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

没有 baseUrl 的 paths

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

 

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

 

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

checkJs 隐含 allowJs

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

 

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

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

JSDoc @see 标签的编辑器支持

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

// @filename: first.tsexport class C { }

// @filename: main.tsimport * as first from './first';

/** * @see first.C */function related() {
复制代码

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

重大更改

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.tsexport * from "./nested/base";

// @filename: projectRoot/nested/base.tsexport 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


2020 年 11 月 13 日 12:552390

评论

发布
暂无评论
发现更多内容

LeetCode题解:226. 翻转二叉树,递归,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

架构师训练营第四周学习总结

尹斌

有这些要素,架构才完整

北风

架构 架构师之道 架构方法

干货 | 全面解析“数字经济”

CECBC区块链专委会

数字经济 经济 经济建设

架构师训练营第三小结(9.28-10.4)

zjzj2017

spring-boot-route(九)整合JPA操作数据库

Java旅途

Java Spring Boot jpa

发几张国庆的照片

亨利笔记

容器 k8s Harbor 镜像

架构师训练营第 1 期第 4 周学习总结

好吃不贵

架构师1期-代码重构作业

ltl3884

极客大学架构师训练营

第四周作业

极客大学架构师训练营

架构师训练营第四周作业

尹斌

Redis-技术专题- 热点Key如何解决

李浩宇/Alex

四面阿里成功定级P6,月薪36K,分享面经(含面试题答案)

Java成神之路

Java 阿里巴巴 程序员 算法 编程语言

月薪60k的Java开发在阿里是什么级别?对技术能力有哪些要求?

Java成神之路

Java 阿里巴巴 程序员 面试 编程语言

实用威胁建模指南(一)

亚伦碎语

敏捷 安全设计 系统安全 #威胁建模

入行架构师之前,这7项技能你要先了解一下

Java架构师迁哥

极客时间架构 1 期:第 3 周代码重构 - 命题作业

Null

单例模式

魏小龙

架构师训练营第 1 期第 4 周作业

好吃不贵

极客大学架构师训练营

【第三周】课后作业

云龙

Hazelcast IMDG 带你瞬间进入内存计算的时代

张磊

分布式计算 内存管理 分布式缓存 分布式内存网格

架构师训练营第三周课后作业

Gosling

极客大学架构师训练营

爆赞!这份《Java核心宝典》绝对是面试复习的最佳选择

Java架构之路

Java 程序员 面试 编程语言

架构师训练营第三周学习总结

Gosling

极客大学架构师训练营

3. CocoaPods 命令解析 - CLAide

Edmond

ruby ios objective-c CocoaPods PackageManager

【第三周】代码重构

云龙

云原生虚机应用托管-设计篇

8小时

2N方定点算法

刀斧手何在

php 数据库 分布式 算法 后端

架构师训练营第三周作业(9.28-10.4)

zjzj2017

Redis-技术专题-基础介绍

李浩宇/Alex

第三节课后作业

happy

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