【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

重磅!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:005097
用户头像
王文婧 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
回复
没有更多了
发现更多内容

极客时间架构训练营模块四作业

现在不学习马上变垃圾

架构训练营10期

Apache Flink 社区 2022 年度报告:Evolution, Diversity, Connection

Apache Flink

大数据 flink 实时计算

拆分电商系统为微服务

闲人Eric

#架构实战营

PyTorch指定GPU进行训练

代码的路

Python

AIGC神器CLIP:技术详解及应用示例

Baihai IDP

人工智能 AI AIGC CLIP

剖析容器网络Flannel VXLAN模式工作原理

Chank

flannel #Kubernetes# #k8s

Studio One2023免费中文版安装下载

茶色酒

Studio One 5 Studio One2023

FL Studio23最新永久版水果软件下载教程

茶色酒

FL Studio2023

ROS常用指令

代码的路

ROS

醉爱江南,2023走进双山香山

科技大数据

基于幂等表思想的幂等实践

小小怪下士

Java 程序员

「Go框架」抽丝剥茧:探究iris路由的底层存储结构

Go学堂

golang 开源 程序员 个人成长

ROS教程(Xavier)

代码的路

ROS

MySql 应用程序常见问题

Andy

模块四:存储架构模式

程序员小张

架构实战营第 10 期 - 模块四作业:设计千万级学生管理系统的考试试卷存储方案

kaizen

「架构实战营」

ETL 引擎 engine 适配 elasticsearch

weigeonlyyou

oracle Prometheus ETL Elastic Search InfluxDB Cluster

数据分析设计模式

agnostic

数据分析模式

千万学生管理系统存储架构设计

陈天境

如何进行秒杀场景下的异步下单实现

风铃架构日知录

Java 数据库 程序员 后端 IT

关于Linux内存回收的几个问题

SkyFire

内存 Linux Kenel

Java中时间戳的使用

代码的路

Java

【Dubbo3终极特性】「云原生三中心架构」带你探索Dubbo3体系下的配置中心和元数据中心、注册中心的原理及开发实战(上)

洛神灬殇

云原生 注册中心 配置中心 Dubbo3 元数据中心

【面试题】Java 2个(多个)大整数相加如何实现

石臻臻的杂货铺

Java’

2023-01-15:销售员。编写一个SQL查询,报告没有任何与名为 “RED” 的公司相关的订单的所有销售人员的姓名。以 任意顺序 返回结果表。 DROP TABLE IF EXISTS `com

福大大架构师每日一题

数据库 sql 福大大

PowerShell 中运行 maven 参数无法识别

HoneyMoose

Design a limited-time offers e-commerce system

David

架构实战营

MASA Stack 1.0 发布会 倒计时一天

MASA技术团队

.net MASA

谈一谈有关 MySQL 数据库数据安全问题

风铃架构日知录

Java MySQL 程序员 后端 IT

SpringBoot配置文件application

代码的路

Java

架构作业4-千万级学生管理系统的考试试卷存储方案

梁山伯

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