NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

重磅!TypeScript 3.7 RC 发布,备受瞩目的 Optional Chaining 来了

  • 2019-10-29
  • 本文字数:8378 字

    阅读完需:约 27 分钟

重磅!TypeScript 3.7 RC发布,备受瞩目的Optional Chaining来了

近日,微软发布了 TypeScript 3.7 RC,这是 TypeScript 3.7 的候选发布版本。到最终版本发布之前,除了重要的错误修复,微软表示,预计不会再有其他更改。下面我们来逐一看看有哪些新功能值得关注。


TypeScript 3.7 RC 提供了一些呼声最高的功能特性!


  • 可选链

  • 空值合并

  • 断言函数

  • 更好地支持返回 never 的函数

  • (更多)递归类型别名

  • –declaration 和 --allowJs

  • 使用项目引用进行免构建编辑

  • 未调用函数的检查

  • TypeScript 文件中的 // @ts-nocheck

  • 分号格式化选项

  • 重大更改

  • DOM 更改

  • 函数真实性检查

  • 本地和导入的类型声明现在会冲突

  • API 更改


你可以通过 NuGet 或以下 npm 命令开始使用 RC 版:


npm install typescript@rc
复制代码


你可以通过下列途径获得编辑器支持:



下面,我们来逐一介绍 TypeScript 3.7 的新功能。首先是最受瞩目的功能:可选链(Optional Chaining)

可选链

TypeScript 3.7 实现了呼声最高的 ECMAScript 功能之一:可选链(Optional Chaining)!我们的团队一直在深度参与 TC39 的标准制定,努力将这一功能推向第三阶段,从而将其带给所有的 TypeScript 用户。


那么什么是可选链呢?从本质上讲,有了可选链后,我们编写代码时如果遇到 null 或 undefined 就可以立即停止某些表达式的运行。可选链的核心是新的?. 运算符,用于 可选的属性访问。比如下面的代码:


let x = foo?.bar.baz();
复制代码


也就是说,当定义了 foo 时,将计算 foo.bar. baz();但如果 foo 为 null 或 undefined,程序就会停止运行并只返回 undefined。说得更清楚些,以上代码等效下面这种写法。


let x = (foo === null || foo === undefined) ?    undefined :    foo.bar.baz();
复制代码


请注意,如果 bar 为 null 或 undefined,我们的代码在访问 baz 时仍会出错。同样,如果 baz 为 null 或 undefined,我们在调用函数时也会出现错误。?. 只会检查其左侧的值是否为 null 或 undefined,而不检查任何后续属性。


你可能会使用?. 替换许多使用 && 运算符执行中间属性检查的代码。


// 之前if (foo && foo.bar && foo.bar.baz) {    // ...}
// 之后if (foo?.bar?.baz) { // ...}
复制代码


请记住?. 与 && 操作的行为有所不同,其中 && 操作特别针对的是“虚假”值(例如空字符串、0、NaN 和 false)。


可选链还包括其他两个操作。首先是 可选元素访问,其作用类似可选属性访问,但允许我们访问非标识符属性(例如任意字符串、数字和符号):


/** * 当我们有一个数组时,返回它的第一个元素 * 否则返回 undefined。 */function tryGetFirstElement<T>(arr?: T[]) {    return arr?.[0];    // 等效:    // return (arr === null || arr === undefined) ?    // undefined :    // arr[0];}
复制代码


另一个是 可选调用,如果表达式不是 null 或 undefined,我们可以有条件地调用它们。


async function makeRequest(url: string, log?: (msg: string) => void) {    log?.(`Request started at ${new Date().toISOString()}`);    // 等效:    // if (log !== null && log !== undefined) {    // log(`Request started at ${new Date().toISOString()}`);    // }
const result = (await fetch(url)).json();
log?.(`Request finished at at ${new Date().toISOString()}`);
return result;}
复制代码


可选链的“短路(short-circuiting)”行为仅限于“普通”和可选属性访问、调用和元素访问——它不会从这些表达式进一步扩展。换句话说,下面的代码不会阻止除法或 someComputation() 调用。


let result = foo?.bar / someComputation()
复制代码


它相当于:


let temp = (foo === null || foo === undefined) ?    undefined :    foo.bar;
let result = temp / someComputation();
复制代码


这可能会导致除法结果 undefined,这就是为什么在 strictNullChecks 中,以下代码会报错:


function barPercentage(foo?: { bar: number }) {    return foo?.bar / 100;    // ~~~~~~~~    // 错误:对象可能未定义。}
复制代码


有关更多详细信息可以查阅提案和原始的拉取请求

空值合并

另一个即将推出的 ECMAScript 功能是 空值合并运算符(nullish coalescing operator),它与可选链都是我们的团队一直努力推进的功能。


你可以设想一下这个功能——?? 运算符可以在处理 null 或 undefined 时“回退”到一个默认值上。例如下面的代码:


let x = foo ?? bar();
复制代码


这是一种新的途径来表示值 foo“存在”时会被使用;但当它为 null 或 undefined 时,就在其位置计算 bar()。


同样,以上代码等效于下面的写法。


let x = (foo !== null && foo !== undefined) ?    foo :    bar();
复制代码


尝试使用默认值时,?? 运算符可以代替||。例如,以下代码段尝试获取上次保存在 local-Storage 中的 volume 值(如果曾经保存过),但因为它使用了||,所以会报错。


function initializeAudio() {    let volume = localStorage.volume || 0.5
// ...}
复制代码


当 localStorage.volume 设置为 0 时,页面会意外地将 volume 设置为 0.5。?? 能避免将 0、NaN 和""中的某些意外行为视为虚假值。


我们非常感谢社区成员 Wenlu Wang 和 Titian Cernicova Dragomir 实现此功能!有关更多详细信息,请查看其拉取请求空值合并提案存储库

断言函数

如果发生意外情况,有一组特定的函数会 throw 一个错误。它们被称为“断言”函数。例如,Node.js 为此有一个专用函数,称为 assert。


assert(someValue === 42);
复制代码


在此示例中,如果 someValue 不等于 42,则 assert 将抛出 AssertionError。


JavaScript 中的断言通常用于防止传入不正确的类型。例如:


function multiply(x, y) {    assert(typeof x === "number");    assert(typeof y === "number");
return x * y;}
复制代码


不幸的是,在 TypeScript 中,这些检查永远无法正确编码。这意味着 TypeScript 对松散类型的代码检查更少,并经常在稍保守的代码类型中迫使用户使用类型断言。


function yell(str) {    assert(typeof str === "string");
return str.toUppercase(); // 哇!我们拼错了 'toUpperCase'。 // 希望 TypeScript 还是能捕获它!}
复制代码


替代方法是重写代码以便 TS 语言分析,但这并不方便。


function yell(str) {    if (typeof str !== "string") {        throw new TypeError("str should have been a string.")    }    // 错误捕获!    return str.toUppercase();}
复制代码


TypeScript 的终极目标是以破坏最少的方式嵌入现有的 JavaScript 结构中,所以 TypeScript 3.7 引入了一个称为“断言签名”的新概念,可以对这些断言函数建模。


第一种断言签名的建模方式和 Node 的 assert 函数的机制相同。它确保在作用域的其余部分中,无论检查的是什么条件都必须为真。


function assert(condition: any, msg?: string): asserts condition {    if (!condition) {        throw new AssertionError(msg)    }}
复制代码


asserts condition 表示,如果 assert 返回,则传递给 condition 参数的任何内容都必须为真(否则会抛出错误)。这意味着对于作用域的其余部分,该条件必须是真的。举个例子,使用这个断言函数意味着我们 确实 捕获了之前 yell 示例中的异常。


function yell(str) {    assert(typeof str === "string");
return str.toUppercase(); // ~~~~~~~~~~~ // 错误:属性 'toUppercase' 在 'string' 类型上不存在。 // 你说的是 'toUpperCase' 吗?}
function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new AssertionError(msg) }}
复制代码


另一种断言签名不检查条件,而是告诉 TypeScript 特定的变量或属性具有不同的类型。


function assertIsString(val: any): asserts val is string {    if (typeof val !== "string") {        throw new AssertionError("Not a string!");    }}
复制代码


这里的 assert val is string 确保在对 assertIs-String 进行任何调用之后,传入的任何变量都将是一个 string。


function yell(str: any) {    assertIsString(str);
// 现在 TypeScript 知道 'str' 是一个 'string'.
return str.toUppercase(); // ~~~~~~~~~~~ // 错误:属性 'toUppercase' 在 'string' 类型上不存在。 // 你说的是 'toUpperCase' 吗?
}
复制代码


这些断言签名与编写类型谓词签名非常相似:


function isString(val: any): val is string {    return typeof val === "string";}
function yell(str: any) { if (isString(str)) { return str.toUppercase(); } throw "Oops!";}
复制代码


就像类型谓词签名一样,这些断言签名也有着惊人的表现力。我们可以用它们表达一些相当复杂的想法。


function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {    if (val === undefined || val === null) {        throw new AssertionError(            `Expected 'val' to be defined, but received ${val}`        );    }}
复制代码


有关断言签名的更多信息,请查看原始的拉取请求

更好地支持返回 never 的函数

作为断言签名项目的一部分,TypeScript 需要对调用的函数和调用的位置编码更多信息。于是我们就可以扩展对另一类函数的支持:返回 never 的函数。


任何返回 never 的函数的意图都是说它不会返回。它表明抛出了异常,出现了暂停错误条件或程序已退出。例如,@types/node 中的 process.exit(…) 被指定为返回 never。


为了确保函数永远不会返回 undefined,或者说可以从所有代码路径中有效地返回,TypeScript 需要一些语法信号——在函数结尾 return 或 throw。这样用户就会发现自己正在 return 故障函数。


function dispatch(x: string | number): SomeType {    if (typeof x === "string") {        return doThingWithString(x);    }    else if (typeof x === "number") {        return doThingWithNumber(x);    }    return process.exit(1);}
复制代码


现在,当调用这些返回 never 的函数时,TypeScript 会识别出它们会影响控制流图,并说明原因。


function dispatch(x: string | number): SomeType {    if (typeof x === "string") {        return doThingWithString(x);    }    else if (typeof x === "number") {        return doThingWithNumber(x);    }    process.exit(1);}
复制代码


你可以在断言函数的拉取请求中查阅更多信息。

(更多)递归类型别名

“递归”引用类型别名时一直存在一些限制。原因是一旦用到类型别名就必须能用其别名来代替自己。在某些情况下这是不可能的,因此编译器会拒绝某些递归别名,如下所示:


type Foo = Foo;
复制代码


这是一个合理的限制,因为任何对 Foo 的使用都需要用 Foo 代替,而 Foo 则需要用 Foo 代替,而 Foo 则需要用 Foo 代替……好吧,希望你懂我的意思!最后没有哪一种类型可以代替 Foo。


这与其他语言对待类型别名的方式一致,但用户使用这一功能时会出现一些意想不到的问题。例如,在 TypeScript 3.6 和更低版本中,以下代码会报错。


type ValueOrArray<T> = T | Array<ValueOrArray<T>>;// ~~~~~~~~~~~~// 错误:类型别名 'ValueOrArray' 循环引用它自己。
复制代码


这很奇怪,因为从技术上讲这种用法并没有错,用户应该总是可以通过引入接口来编写实际上是相同的代码。


type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;
interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}
复制代码


由于接口(和其他对象类型)引入了一定程度的间接性,并且不需要急切地构建其完整结构,因此 TypeScript 在使用该结构时没有问题。


但对于用户而言,引入界面的解决方法并不直观。原则上来说,ValueOrArray 的原始版本,也就是直接使用 Array 确实没有任何问题。如果编译器有点“懒惰”,并且仅在必要时才计算 Array 的类型参数,则 TypeScript 就可以正确表达这些参数。


这正是 TypeScript 3.7 引入的内容。在类型别名的“顶层”,TypeScript 将推迟解析类型参数以允许使用这些模式。


这意味着像下面这样的代码(试图表示 JSON)……


type Json =    | string    | number    | boolean    | null    | JsonObject    | JsonArray;
interface JsonObject { [property: string]: Json;}
interface JsonArray extends Array<Json> {}
复制代码


终于无需辅助接口也能重写了。


type Json =    | string    | number    | boolean    | null    | { [property: string]: Json }    | Json[];
复制代码


这种新的宽松条件也使我们可以在元组中递归引用类型别名。下面的代码以前会报错,现在则是有效的 TypeScript 代码:


type VirtualNode =    | string    | [string, { [key: string]: any }, ...VirtualNode[]];
const myNode: VirtualNode = ["div", { id: "parent" }, ["div", { id: "first-child" }, "I'm the first child"], ["div", { id: "second-child" }, "I'm the second child"] ];
复制代码


有关更多信息可以查阅原始的拉取请求

–declaration 和 --allowJs

TypeScript 中的 --declaration 标志允许我们从源 TypeScript 文件(如.ts 和.tsx 文件)生成.d.ts 文件(声明文件)。这些.d.ts 文件很重要,因为它们允许 TypeScript 对其他项目进行类型检查,而无需重新检查 / 构建原始源代码。出于相同的原因,使用项目引用时 需要 此设置。


不幸的是,–declaration 不适用于 --allowJs 之类的设置,无法混合使用 TypeScript 和 JavaScript 输入文件。这是一个令人沮丧的限制,因为它意味着用户即使在迁移代码库时也无法使用 --declaration,即使使用 JSDoc 注释也是如此。TypeScript 3.7 对此做了更改,并允许将这两种功能混合使用!


使用 allowJs 时,TypeScript 将尽可能理解 JavaScript 源代码,并将其以等效表示形式保存到.d.ts 文件中。这包括其中所有的 JSDoc 注释,所以像如下代码:


/** * @callback Job * @returns {void} */
/** Queues work */export class Worker { constructor(maxDepth = 10) { this.started = false; this.depthLimit = maxDepth; /** * 注意:队列中的作业可能会将更多项目添加到队列中 * @type {Job[]} */ this.queue = []; } /** * 在队列中添加一个工作项 * @param {Job} work */ push(work) { if (this.queue.length + 1 > this.depthLimit) throw new Error("Queue full!"); this.queue.push(work); } /** * 如果队列尚未开始就启动它 */ start() { if (this.started) return false; this.started = true; while (this.queue.length) { /** @type {Job} */(this.queue.shift())(); } return true; }}
复制代码


现在将转换为以下无需实现的.d.ts 文件:


/** * @callback Job * @returns {void} *//** Queues work */export class Worker {    constructor(maxDepth?: number);    started: boolean;    depthLimit: number;    /**     * 注意:队列中的作业可能会将更多项目添加到队列中     * @type {Job[]}     */    queue: Job[];    /**     * 在队列中添加一个工作项     * @param {Job} work     */    push(work: Job): void;    /**     * 如果队列尚未开始就启动它     */    start(): boolean;}export type Job = () => void;
复制代码


有关更多详细信息请查阅原始拉取请求

使用项目引用进行免构建编辑

TypeScript 的项目引用为我们提供了一种简便的方法来分解代码库,从而提升编译速度。不幸的是,编辑一个尚未建立依赖关系(或输出已过时)的项目时会出现很多问题。


在 TypeScript 3.7 中,当打开具有依赖项的项目时,TypeScript 将自动使用源.ts / .tsx 文件代替。这意味着使用项目引用的项目现在将获得增强的编辑体验,其中语义操作都是最新且“有效”的。你可以使用编译器选项 disableSourceOfProjectReferenceRedirect 禁用此行为,处理非常大的项目(此更改可能影响编辑性能)时用这个选项可能会很合适。


有关此更改的更多信息请查阅其拉取请求

未调用函数的检查

一个常见且危险的错误是忘记调用某个函数,尤其是当函数具有零参数,或者命名更像是属性而非函数时更容易出错。


interface User {    isAdministrator(): boolean;    notify(): void;    doNotDisturb?(): boolean;}
// 稍后……
// 代码已破坏, 勿用!function doAdminThing(user: User) { // oops! if (user.isAdministrator) { sudo(); editTheConfiguration(); } else { throw new AccessDeniedError("User is not an admin"); }}
复制代码


比如在上面的代码中我们忘记调用 isAdmini-strator,该代码错误地允许非管理员用户编辑配置!


在 TypeScript 3.7 中,这被标识为可能的错误:


function doAdminThing(user: User) {    if (user.isAdministrator) {    // ~~~~~~~~~~~~~~~~~~~~    // 错误!这个条件将始终返回 true,因为这个函数定义是一直存在的    // 你的意思是调用它吗?
复制代码


此检查是一项重大更改,但正因如此,检查会非常保守。仅在 if 条件下才发出此错误,并且在 strictNullChecks 关闭,或者以后在 if 的正文中调用该函数时,不会在可选属性上发出此错误:


interface User {    isAdministrator(): boolean;    notify(): void;    doNotDisturb?(): boolean;}
function issueNotification(user: User) { if (user.doNotDisturb) { // OK, 属性是可选的 } if (user.notify) { // OK, 调用了这一函数 user.notify(); }}
复制代码


如果你打算在不调用函数的情况下测试它,则可以将其定义更正为包含 undefined/null,或使用!! 编写类似 if(!!user.isAdministrator) 的内容以指示强制是故意的。


我们非常感谢 GitHub 用户 @jwbay,他创建了概念验证,并一直改进到现在这个版本。

TypeScript 文件中的 // @ts-nocheck

TypeScript 3.7 允许我们在 TypeScript 文件的顶部添加 // @ts-nocheck 注释以禁用语义检查。过去只有在存在 checkJs 时,JavaScript 源文件中的这一注释才会被认可。但我们已扩展了对 TypeScript 文件的支持,以简化所有用户的迁移过程。


分号格式化选项 TypeScript 的内置格式化程序现在支持在结尾分号可选的位置,根据 JavaScript 的自动分号插入(ASI)规则插入和删除分号。该设置现在在 Visual Studio Code Insiders 中可用,也在 Visual Studio 16.4 Preview 2 中的“Tools Options”菜单中可用。



选择“insert”或“remove”的值还会影响自动导入的格式、提取的类型以及 TypeScript 服务提供的其他生成代码。将设置保留为默认值“ignore”会使生成的代码与当前文件中检测到的分号首选项相匹配。

重大更改

DOM 更改

lib.dom.d.ts 中的类型已更新。这些更改大都是与可空性相关的正确性更改,但其影响最终取决于你的代码库。


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

函数真实性检查

如上所述,当在 if 语句条件下似乎未调用函数时,TypeScript 现在会出错。除非满足以下任一条件,否则在 if 条件下检查函数类型时会发出错误:


  • 检查值来自可选属性。

  • strictNullChecks 已禁用。

  • 该函数稍后在 if 的正文中调用。

本地和导入的类型声明现在会起冲突

由于一个错误,之前的 TypeScript 版本允许以下构造:


// ./someOtherModule.tsinterface SomeType {    y: string;}
// ./myModule.tsimport { SomeType } from "./someOtherModule";export interface SomeType { x: number;}
function fn(arg: SomeType) { console.log(arg.x); // Error! 'x' doesn't exist on 'SomeType'}
复制代码


在这里,SomeType 似乎同时源于 import 声明和本地 interface 声明。可能想不到的是,在模块内部 SomeType 仅引用 import 的定义,而本地声明 SomeType 仅在从另一个文件导入时才可用。这非常令人困惑,我们对这类罕见情况的调查表明,开发人员通常会认为出现了一些其他问题。


TypeScript 3.7 现在可以正确地将其识别为重复的标识符错误。正确的修复方案取决于作者的初衷,具体情况具体分析。通常来说,命名冲突是无意的,最好的解决方法是重命名导入的类型。如果初衷是要扩展导入的类型,则应编写适当的模块扩展。

API 更改

为了支持前文所述的递归类型别名模式,我们已从 TypeReference 接口中删除了 typeArguments 属性。用户应该在 TypeChecker 实例上使用 getTypeArguments 函数。

未来计划

TypeScript 3.7 的最终版本将在几周内发布,理想情况下只需极少的更改即可完成!我们希望大家尝试这个 RC 版本并向我们提供反馈,从而打造一个出色的 3.7 版本。如果你有任何建议或遇到任何问题,请随时在我们的问题跟踪器上报告问题!


https://github.com/microsoft/TypeScript/issues/new/choose


编程快乐!


原文链接:


https://devblogs.microsoft.com/typescript/announcing-typescript-3-7-rc/


2019-10-29 08:005099
用户头像
王文婧 InfoQ编辑

发布了 126 篇内容, 共 70.6 次阅读, 收获喜欢 275 次。

关注

评论 3 条评论

发布
用户头像
太香了.博主写的很棒.为了感谢博主推荐博主使用一款国产免费接口管理工具-ApiPost.辛苦辛苦
2021-08-26 16:54
回复
用户头像
好些个是源自 C# 的语法糖特性啦
2020-02-10 21:33
回复
用户头像
跟 Dart 越来越像了,Null 调用 ?.、Null 判断 ??、断言 assert ...
2019-10-29 10:40
回复
没有更多了
发现更多内容

2021年阿里Java高级面试题分享

策划Java工程师

Java 程序员 后端

2021最新Java开发者学习路线

策划Java工程师

Java 程序员 后端

手撕环形队列系列三:多生产者并行写入

实力程序员

程序员 数据结构 C语言 编程开发 环形队列

十大排序算法--希尔排序

Ayue、

排序算法 8月日更

用微服务架构方式交付云服务产品

用友BIP

容器 微服务 专属云

2021年最新大厂Java面试笔试题目

策划Java工程师

Java 程序员 后端

取经之旅第 55 天,Python OpenCV 透视变换前置知识轮廓坐标点

梦想橡皮擦

8月日更

百度第25季黑客马拉松再秀“技术基因”,累计产生创意超7000个

科技热闻

容器云平台和Kubernetes之间不得不说的那些事

用友BIP

Kubernetes 容器

企业需要拥有自己特色的DevOps

用友BIP

Docker 容器 DevOps 微服务

【LeetCode】有效三角形的个数Java题解

Albert

算法 LeetCode 8月日更

上K8s,研发团队如何从容一点?

行云创新

容器 k8s

2021年字节跳动+京东+美团面试总结

Java 程序员 后端

2021最新Java中级面试题目汇总解答

策划Java工程师

Java 程序员 后端

手撸二叉树之二叉搜索树的最近公共祖先

HelloWorld杰少

数据结构与算法 8月日更

springcloud 微服务日志写入kafka

Rubble

kafka Spring Cloud 8月日更

2021年抓住金三银四涨薪好时机

Java 程序员 后端

2021年最新Java大厂面试题来袭

策划Java工程师

Java 程序员 后端

2021年阿里Java高级面试题及答案

策划Java工程师

Java 程序员 后端

极客-大数据-作业4 Hive

西伯利亚鼯鼠

kubernetes入门:简介与基础操作命令

小鲍侃java

8月日更

DDD 领域驱动设计·学习应用·二

小诚信驿站

领域驱动设计 领域 领域驱动模型DDD 中台架构 领域驱动

2021年大厂Java面经

Java 程序员 后端

2021必看-Java高级面试题总结

策划Java工程师

Java 程序员 后端

【Flutter 专题】78 图解 Android Native 集成 FlutterBoost 小尝试 (一)

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 8月日更

面试官:展开说说,Spring中Bean对象是如何通过注解注入的?

小傅哥

spring 小傅哥 注解注入

2021年春招Java面试题

Java 程序员 后端

2021我的Java路要怎么走

策划Java工程师

Java 程序员 后端

netty系列之:netty中的ByteBuf详解

程序那些事

Java Netty nio 程序那些事

2021最值得加入的互联网公司有哪些

策划Java工程师

Java 程序员 后端

一次Http Get请求健壮性问题的排查过程

liuzhen007

8月日更

重磅!TypeScript 3.7 RC发布,备受瞩目的Optional Chaining来了_大前端_Daniel Rosenwasser_InfoQ精选文章