TypeScript 3.7 Beta 版发布

咱们很高兴发布 TypeScript 3.7 Beta 版,它包含了 TypeScript 3.7 版本的全部功能。从如今到最后发布以前,咱们将修复错误并进一步提升它的性能和稳定性。php

开始使用 Beta 版,你能够经过 NuGet 安装,或者经过 npm 使用如下命令安装:前端

npm install typescript@beta
复制代码

你还能够经过如下方式获取编辑器的支持node

TypeScript 3.7 Beta 版包括了开发者呼声最高的一些功能!让咱们深刻研究一下新功能,从 3.7:可选链(Optional Chaining)开始。android

可选链(Optional Chaining)

TypeScript 3.7 实现了迄今为止需求声最高的 ECMAScript 功能之一:可选链!咱们的团队成员一直都在高度参与 TC39 委员会,努力争取将这个新功能加入到 ECMAScript 提案的第三个阶段,以便在将来咱们能够将其带给全部的 TypeScript 用户。ios

那什么是可选链呢?从本质上讲,可选链使咱们在编写代码时,若是遇到 null 或者 undefined,能够当即中止运行某些表达式。可选链的主角是这个为了可选属性访问而存在的新运算符 ?.。当咱们像下面这样写代码时:git

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

也就是说,当 foo 被定义时,foo.bar.baz() 将会被计算;可是当 foonull 或者 undefined 时,停下来不继续执行,直接返回 undefinedgithub

更明确地说,上面那段代码的意思和下面的这段彻底相同。typescript

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

注意,若是 barnull 或者 undefined,在咱们的代码尝试访问 baz 时,它仍然会出错。一样,若是 baznull 或者 undefined,在咱们调用这个函数时也会报错。?. 仅仅检查在它左边的值是否为 null 或者 undefined —— 不包括在它以后的任何一个属性。npm

你可能会发现你用 ?. 替换了不少使用 && 运算符执行中间属性检查的代码。json

// 以前
if (foo && foo.bar && foo.bar.baz) {
    // ...
}

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

请牢记 ?. 不一样于 && 运算符,由于 && 仅仅是针对那些“假”(转换为布尔值为假)数据(例如:空字符串、0NaN 以及 false)。

可选链还包括其余两个操做。首先是可选元素访问,其做用相似于可选属性访问,但容许咱们访问非属性标识符属性(例如:任意字符串、数字和 Symbol)

/** * 当咱们有一个数组时,返回它的第一个元素 * 不然返回 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;
}
复制代码

可选链具备的“短路”行为仅限于“普通”和可选属性的访问、调用以及可选元素的访问 —— 不会在表达式的基础上进一步扩展。换句话说,

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

不会阻止除法或者调用 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;
    // ~~~~~~~~
    // 错误:对象有可能未定义。
}
复制代码

更多的细节,你能够阅读该提案 或者 查看原始的 pull request

空值合并(Nullish Coalescing)

空值合并运算符是另外一个即将到来的 ECMAScript 新功能,和可选链是一对好兄弟,咱们团队也在努力争取(将这个新功能加入到 ECMAScript 提案的第三个阶段)。

你能够考虑使用这个功能 —— ?? 运算符 —— 做为一种处理 null 或者 undefined 时“回退”到默认值的方法。当咱们像下面这样写代码时

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

这是一种新的表达方式,告诉咱们,当 foo “存在”时使用 foo;但当它是 null 或者 undefined 时,在它的位置上计算 bar() 的值。

一样,上面的代码和下面的等价。

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

当咱们尝试使用默认值时,?? 运算符能够代替 ||。例如,下面的代码会尝试获取上次保存在 localStorage 中的 volume 值(若是曾经保存过);可是因为使用 || 这里存在一个 bug。

function initializeAudio() {
    let volume = localStorage.volume || 0.5

    // ...
}
复制代码

localStorage.volume 被置为 0 时,页面会意外地将 0.5 赋给 volume。?? 能够避免一些由 0 致使的意外行为,NaN"" 都会被 ?? 认为是假。

很是感谢社区成员 Wenlu WangTitian Cernicova Dragomir 实现这个功能!更多的细节,你能够查看他们的 pull request 或者 查看空值合并提案仓库

断言函数

当错误发生的时候,一组特定的函数会 throw(抛出)异常。它们被称为“断言”函数。例如,Node.js 为此有一个专用函数,称为 assert

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

在这个例子中,若是 someValue 不等于 42assert 将会抛出一个 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 仍然能捕获了这个错误,那就太好了!
}
复制代码

替代方案是改写代码,以便语言能够对其解析,但这并不方便!

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

最终 TypeScript 的目标是以最小破坏的方法嵌入现有的 JavaScript 结构中。所以,TypeScript 3.7 引入了一个称为“断言签名(assertion signatures)”的新概念,能够对这些断言函数进行建模。

第一种断言签名对 Node 的 assert 函数工做方法进行建模。它确保在函数做用域内的其他部分中,不管检查什么条件都必定为真。

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

asserts condition 表示,若是 assert(正常)返回了,那么不管传递给 condition 的参数是什么,它都必定为 true,不然 assert 会抛出一个异常。这意味着对于做用域内的其余部分,这个条件也必定是真的。例如,使用这个断言函数意味着咱们确实捕获了刚才 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!");
    }
}
复制代码

这里 asserts val is string 确保在调用 assertIsString 以后,传入的任何变量都是能够被认为是一个 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}`
        );
    }
}
复制代码

要了解有关断言签名的更多信息,请查看原始 pull request

更好地支持返回 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);
}
复制代码

与断言函数同样,你能够在相同的 pull request 中阅读更多的细节

(更多)递归类型别名

类型别名在如何”递归“引用它们方面一直受到限制。缘由是对类型别名的任何使用都必须可以用其别名替换自身。在某些状况下,这是不可能的,所以编译器会拒绝某些递归别名,以下所示:

type 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 并无任何错误。若是编译器有点“懒惰”,仅在必要的时候才计算类型参数,那么 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"]
    ];
复制代码

更多的细节,你能够阅读原始的 pull request

--declaration--allowJs

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

不幸的是,--declaration 不能和 --allowJs(容许混合 TypeScript 和 JavaScript 的输入文件) 一块儿使用。这是一个使人沮丧的限制,由于它意味着即使是 JSDoc 注释,在用户在迁移代码库时也没法使用。

在使用 allowJs 时,TypeScript 将尽最大努力理解 JavaScript 源代码,并将其以等效的表达形式存储在一个 .d.ts 文件中。这包括它全部的 JSDoc 注释,因此像下面这样的代码:

/** * @callback Job * @returns {void} */

/** 工做队列 */
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}
 */
/** 工做队列 */
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;
复制代码

更多的细节,你能够查看原始的 pull request

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

TypeScript 的项目引用为咱们提供了一种简单的方法来分解代码库,从而使咱们能够更快地进行编译。不幸的是,编辑还没有创建依赖关系(或者输出过期)的项目意味着这种编辑体验没法正常工做。

在 TypeScript 3.7 中,当打开具备依赖项的项目时,TypeScript 将自动使用源 .ts/.tsx 文件代替。这意味着使用项目引用的项目如今将得到更好的编辑体验,其中语义化操做是最新且“有效”的。在很是大的项目中使用这个更改可能会影响编辑性能,你可使用编译器选项 disableSourceOfProjectReferenceRedirect 禁用此行为。

你能够经过阅读原始的 pull request 来了解有关这个更改的更多信息

未调用的函数检查

忘记调用函数是一个常见且危险的错误,特别是当函数没有参数或者以一种暗示它多是属性而不是函数的方式命名时。

interface User {
    isAdministrator(): boolean;
    notify(): void;
    doNotDisturb?(): boolean;
}

// 稍后……

// 有问题的代码,请勿使用!
function doAdminThing(user: User) {
    // 糟糕!
    if (user.isAdministrator) {
        sudo();
        editTheConfiguration();
    }
    else {
        throw new AccessDeniedError("User is not an admin");
    }
}
复制代码

在这里,咱们忘记了调用 isAdministrator,该代码将错误地容许非管理员用户编辑配置!

在 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 文件的支持,以使全部用户的迁移更加容易。

分号格式化选项

因为 JavaScript 的自动分号插入(ASI)规则,TypeScript 的内置格式化程序如今支持在分号结尾可选的位置插入和删除分号。该设置如今在 Visual Studio Code Insiders 中可用,能够在 Visual Studio 16.4 Preview 2 中的“工具选项”菜单中找到它。

VS Code 中新的分号格式化选项

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

重大变动

DOM 变动

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

函数为真检查

如上所述,当在 if 语句条件内存在函数,且看起来彷佛没有被调用时,TypeScript 如今会报错。在 if 条件中检查函数类型时,将产生错误,除非知足如下任何条件:

  • 检查值来自可选属性
  • strictNullChecks 被禁用
  • 该函数稍后在 if 中被调用

本地和导入类型声明如今会发生冲突

以前因为存在 bug,TypeScript 容许如下构造:

// ./someOtherModule.ts
interface SomeType {
    y: string;
}

// ./myModule.ts
import { SomeType } from "./someOtherModule";
export interface SomeType {
    x: number;
}

function fn(arg: SomeType) {
    console.log(arg.x); // 错误!'SomeType' 上不存在 'x'
}
复制代码

在这里,SomeType 彷佛起源于 import 声明和本地的 interface 声明。也许使人惊讶的是,在模块内部,SomeType 只是引用了被 import 的定义,而本地声明的 SomeType 仅在从另外一个文件导入时才可用。这很是使人困惑,咱们对极少数这种状况的代码进行的野蛮审查代表,开发人员一般认为正在发生一些不一样的事情。

在 TypeScript 3.7 中,如今能够正确地将其标识为重复标识符错误。正确的解决方案取决于做者的初衷,并应逐案解决。一般,命名冲突是无心的,最好的解决方法是重命名导入的类型。若是要扩展导入的类型,则应编写适当的模块进行扩展。

下一步

TypeScript 3.7 的最终版本将在 11 月初发布,在那以前的几周将发布候选版本。咱们但愿您能试用一下 Beta 版,并让咱们知道它工做的如何。若是您有任何建议或遇到任何问题,请尽情前往问题跟踪页面并提出新问题

Happy Hacking!

—— Daniel Rosenwasser 和 TypeScript 团队

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索