Optional Chaining #

Playground

可选链接是问题跟踪器上的问题16 . 从上下文来看,自那时以来,TypeScript问题跟踪器上已有23,000个问题.

从本质上讲,可选链接使我们可以编写代码,如果遇到nullundefined ,TypeScript可以立即停止运行某些表达式. 可选链接中节目的主角是新的?. 用于可选属性访问的运算符. 当我们写像

let x = foo?.bar.baz();

这就是说,当定义了foo时,将计算foo.bar.baz() ; 但是当foonullundefined ,停止我们正在做的事情,只返回undefined ."

更明确地说,该代码段与编写以下代码相同.

let x = (foo === null || foo === undefined) ?
    undefined :
    foo.bar.baz();

请注意,如果barnullundefined ,我们的代码访问baz仍然会出错. 同样,如果baznullundefined ,我们将在呼叫站点出现错误. ?. 仅检查其左侧的值是否为nullundefined -不检查任何后续属性.

您可能会发现自己使用?. 替换许多使用&&运算符执行重复无效检查的代码.

// Before
if (foo && foo.bar && foo.bar.baz) {
    // ...
}

// After-ish
if (foo?.bar?.baz) {
    // ...
}

请记住那个?.&&操作的行为有所不同,因为&&将特别对"虚假"值(例如,空字符串0NaN ,以及false )起作用,但这是构造的故意特征. 它不会短路有效数据(例如0或空字符串).

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

/**
 * Get the first element of the array if we have an array.
 * Otherwise return undefined.
 */
function tryGetFirstElement<T>(arr?: T[]) {
    return arr?.[0];
    // equivalent to
    //   return (arr === null || arr === undefined) ?
    //       undefined :
    //       arr[0];
}

还有一个可选的调用 ,它允许我们有条件地调用表达式,如果它们不是nullundefined .

async function makeRequest(url: string, log?: (msg: string) => void) {
    log?.(`Request started at ${new Date().toISOString()}`);
    // roughly equivalent to
    //   if (log != null) {
    //       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;
    //     ~~~~~~~~
    // Error: Object is possibly undefined.
}

更多详细信息,您可以阅读该提案查看原始的拉取请求 .

Nullish Coalescing #

Playground

无效的合并运算符是另一个即将推出的ECMAScript功能,它与可选的链接并驾齐驱,并且我们的团队参与了TC39的倡导工作.

您可以想到此功能- ?? 运算符-一种在处理nullundefined时"回退"到默认值的方法. 当我们写像

let x = foo ?? bar();

这是一种新的方式来表示值foo将在"存在"时使用; 但是当它为nullundefined ,请在其位置计算bar() .

同样,以上代码等效于以下代码.

let x = (foo !== null && foo !== undefined) ?
    foo :
    bar();

?? 运算符可以代替||使用 尝试使用默认值时. 例如,以下代码段尝试获取上次保存在localStorage (如果曾经); 但是,它有一个错误,因为它使用|| .

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

    // ...
}

localStorage.volume设置为0 ,页面会将音量设置为0.5 ,这是意外的. ?? 避免将0NaN""中的某些意外行为视为虚假值.

我们非常感谢社区成员Wang WenluTitian Cernicova Dragomir实施此功能! 有关更多详细信息, 请查看其拉取请求无效的合并提议存储库 .

Assertion Functions #

Playground

如果发生意外情况,有一组特定的函数会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();
    // Oops! We misspelled 'toUpperCase'.
    // Would be great if TypeScript still caught this!
}

替代方法是改写代码,以便语言可以对其进行分析,但这并不方便.

function yell(str) {
    if (typeof str !== "string") {
        throw new TypeError("str should have been a string.")
    }
    // Error caught!
    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参数的任何内容都必须为true(因为否则会引发错误). 这意味着对于其余范围,该条件必须是真实的. 举个例子,使用这个断言函数意味着我们确实抓住了我们原来的yell例子.

function yell(str) {
    assert(typeof str === "string");

    return str.toUppercase();
    //         ~~~~~~~~~~~
    // error: Property 'toUppercase' does not exist on type 'string'.
    //        Did you mean '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);

    // Now TypeScript knows that 'str' is a 'string'.

    return str.toUppercase();
    //         ~~~~~~~~~~~
    // error: Property 'toUppercase' does not exist on type 'string'.
    //        Did you mean '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 .

Better Support for never-Returning Functions #

作为断言签名工作的一部分,TypeScript需要对调用位置和调用函数进行更多编码. 这使我们有机会扩展对另一类功能的支持: never返回的功能.

这样做的目的的任何功能即恢复never是,它永远不会返回. 它表明引发了异常,发生了暂停错误条件或程序已退出. 例如, @types/node process.exit(...)被指定为never返回.

为了确保函数永远不会从所有代码路径中返回undefined或有效返回,TypeScript需要一些语法信号-在函数结尾处returnthrow . 因此,用户发现自己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中阅读更多内容 .

(More) Recursive Type Aliases #

Playground

类型别名在如何"递归"引用它们方面一直受到限制. 原因是对类型别名的任何使用都必须能够用其别名来代替自身. 在某些情况下,这是不可能的,因此编译器拒绝某些递归别名,如下所示:

type Foo = Foo;

这是一个合理的限制,因为任何对Foo使用都需要用Foo代替,而Foo则需要用Foo代替,而Foo则需要用Foo代替……好吧,希望您能理解! 最后,没有什么类型可以代替Foo .

This is fairly consistent with how other languages treat type aliases, but it does give rise to some slightly surprising scenarios for how users leverage the feature. For example, in TypeScript 3.6 and prior, the following causes an error.

type ValueOrArray<T> = T | Array<ValueOrArray<T>>;
//   ~~~~~~~~~~~~
// error: Type alias 'ValueOrArray' circularly references itself.

这很奇怪,因为从技术上讲,任何使用都没有错,用户总是可以通过引入接口来编写实际上是相同代码的代码.

type ValueOrArray<T> = T | ArrayOfValueOrArray<T>;

interface ArrayOfValueOrArray<T> extends Array<ValueOrArray<T>> {}

因为接口(和其他对象类型)引入了一个间接级别,并且不需要急切地构建它们的完整结构,所以TypeScript在使用该结构时没有问题.

但是,对于用户而言,引入界面的解决方法并不直观. 原则上,直接使用ArrayValueOrArray的原始版本确实没有任何问题. 如果编译器有点"懒惰",并且仅在必要时才计算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 and --allowJs #

TypeScript中的--declaration标志允许我们从TypeScript源文件(即.ts.tsx文件)生成.d.ts文件(声明文件). 这些.d.ts文件很重要,原因有两个.

首先,它们很重要,因为它们允许TypeScript对其他项目进行类型检查,而无需重新检查原始源代码. 它们也很重要,因为它们允许TypeScript与未考虑TypeScript构建的现有JavaScript库进行互操作. 最后,通常没有得到充分认识的好处:当使用由TypeScript支持的编辑器来获得更好的自动完成功能时,TypeScript JavaScript用户都可以从这些文件中受益.

不幸的是,-- --declaration无法与--allowJs标志一起使用,该标志允许混合TypeScript和JavaScript输入文件. 这是一个令人沮丧的限制,因为它意味着用户即使在迁移代码库时也无法使用--declaration标志,即使使用JSDoc注释也是如此. TypeScript 3.7对此进行了更改,并允许将这两个选项一起使用!

此功能最有影响力的结果可能有点微妙:使用TypeScript 3.7,用户可以使用JSDoc注释的JavaScript编写库并支持TypeScript用户.

它的工作方式是,在使用allowJs ,TypeScript会进行一些尽力而为的分析以了解常见的JavaScript模式. 但是,用JavaScript表示某些模式的方式不一定看起来像它们在TypeScript中的等效形式. 启用declaration发射后,TypeScript会找出将JSDoc注释和CommonJS导出转换为输出.d.ts文件中的有效类型声明等的最佳方法.

例如,以下代码片段

const assert = require("assert")

module.exports.blurImage = blurImage;

/**
 * Produces a blurred image from an input buffer.
 * 
 * @param input {Uint8Array}
 * @param width {number}
 * @param height {number}
 */
function blurImage(input, width, height) {
    const numPixels = width * height * 4;
    assert(input.length === numPixels);
    const result = new Uint8Array(numPixels);

    // TODO

    return result;
}

将产生一个.d.ts文件,例如

/**
 * Produces a blurred image from an input buffer.
 *
 * @param input {Uint8Array}
 * @param width {number}
 * @param height {number}
 */
export function blurImage(input: Uint8Array, width: number, height: number): Uint8Array;

使用@param标记也可以超越基本功能,例如以下示例:

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

/** Queues work */
export class Worker {
    constructor(maxDepth = 10) {
        this.started = false;
        this.depthLimit = maxDepth;
        /**
         * NOTE: queued jobs may add more items to queue
         * @type {Job[]}
         */
        this.queue = [];
    }
    /**
     * Adds a work item to the queue
     * @param {Job} work 
     */
    push(work) {
        if (this.queue.length + 1 > this.depthLimit) throw new Error("Queue full!");
        this.queue.push(work);
    }
    /**
     * Starts the queue if it has not yet started
     */
    start() {
        if (this.started) return false;
        this.started = true;
        while (this.queue.length) {
            /** @type {Job} */(this.queue.shift())();
        }
        return true;
    }
}

will be transformed into the following .d.ts file:

/**
 * @callback Job
 * @returns {void}
 */
/** Queues work */
export class Worker {
    constructor(maxDepth?: number);
    started: boolean;
    depthLimit: number;
    /**
     * NOTE: queued jobs may add more items to queue
     * @type {Job[]}
     */
    queue: Job[];
    /**
     * Adds a work item to the queue
     * @param {Job} work
     */
    push(work: Job): void;
    /**
     * Starts the queue if it has not yet started
     */
    start(): boolean;
}
export type Job = () => void;

请注意,将这些标志一起使用时,TypeScript不一定必须降级.js文件. 如果只希望TypeScript创建.d.ts文件,则可以使用--emitDeclarationOnly编译器选项.

有关更多详细信息,您可以签出原始拉取请求 .

The useDefineForClassFields Flag and The declare Property Modifier #

返回当TypeScript实现公共类字段时,我们尽力做到了以下代码

class C {
    foo = 100;
    bar: string;
}

等效于构造函数体内的类似分配.

class C {
    constructor() {
        this.foo = 100;
    }
}

不幸的是,虽然这似乎是该提案在早期的发展方向,但极有可能将公共类领域进行不同的标准化. 取而代之的是,原始代码示例可能需要对以下内容进行脱糖处理:

class C {
    constructor() {
        Object.defineProperty(this, "foo", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 100
        });
        Object.defineProperty(this, "bar", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
    }
}

虽然TypeScript 3.7默认情况下不更改任何现有发射,但我们一直在逐步推出更改,以帮助用户减轻将来可能发生的损坏. 我们提供了一个名为useDefineForClassFields的新标志,以使用一些新的检查逻辑来启用此发射模式.

最大的两个变化如下:

  • 声明使用Object.defineProperty初始化.
  • 即使声明没有初始化程序,也始终将其初始化为undefined .

对于使用继承的现有代码,这可能会导致很多后果. 首先,基类的set访问器不会被触发-它们将被完全覆盖.

class Base {
    set data(value: string) {
        console.log("data changed to " + value);
    }
}

class Derived extends Base {
    // No longer triggers a 'console.log' 
    // when using 'useDefineForClassFields'.
    data = 10;
}

其次,使用类字段来专门化基类的属性也不起作用.

interface Animal { animalStuff: any }
interface Dog extends Animal { dogStuff: any }

class AnimalHouse {
    resident: Animal;
    constructor(animal: Animal) {
        this.resident = animal;
    }
}

class DogHouse extends AnimalHouse {
    // Initializes 'resident' to 'undefined'
    // after the call to 'super()' when
    // using 'useDefineForClassFields'!
    resident: Dog;

    constructor(dog: Dog) {
        super(dog);
    }
}

What these two boil down to is that mixing properties with accessors is going to cause issues, and so will re-declaring properties with no initializers.

为了检测访问器周围的问题,TypeScript 3.7现在将在.d.ts文件中发出get / set访问器,以便在TypeScript中可以检查重写的访问器.

受类字段更改影响的代码可以通过将字段初始化程序转换为构造函数主体中的赋值来解决问题.

class Base {
    set data(value: string) {
        console.log("data changed to " + value);
    }
}

class Derived extends Base {
    constructor() {
        data = 10;
    }
}

To help mitigate the second issue, you can either add an explicit initializer or add a declare modifier to indicate that a property should have no emit.

interface Animal { animalStuff: any }
interface Dog extends Animal { dogStuff: any }

class AnimalHouse {
    resident: Animal;
    constructor(animal: Animal) {
        this.resident = animal;
    }
}

class DogHouse extends AnimalHouse {
    declare resident: Dog;
//  ^^^^^^^
// 'resident' now has a 'declare' modifier,
// and won't produce any output code.

    constructor(dog: Dog) {
        super(dog);
    }
}

当前useDefineForClassFields仅在面向ES5和更高版本时可用,因为Object.defineProperty中不存在Object.defineProperty . 为了实现类似的问题检查,您可以创建一个针对ES5的单独项目,并使用--noEmit避免进行完整构建.

有关更多信息,您可以查看这些更改的原始拉取请求 .

我们强烈建议用户尝试使用useDefineForClassFields标志,并在问题跟踪器或以下注释中进行报告. 这包括有关采用该标志的难度的反馈,因此我们可以了解如何使迁移更容易.

Build-Free Editing with Project References #

TypeScript的项目参考为我们提供了一种简单的方法来分解代码库,从而使我们可以更快地进行编译. 不幸的是,编辑尚未建立依赖关系(或输出过时)的项目意味着编辑体验无法正常工作.

在TypeScript 3.7中,当打开具有依赖项的项目时,TypeScript将自动使用源.ts / .tsx文件代替. 这意味着使用项目引用的项目现在将获得改进的编辑体验,其中语义操作是最新的并且"有效". 您可以使用编译器选项disableSourceOfProjectReferenceRedirect禁用此行为,当在非常大的项目中使用该选项可能会影响编辑性能时,该选项可能是适当的.

您可以通过阅读其更改请求来阅读有关此更改的更多信息 .

Uncalled Function Checks #

常见且危险的错误是忘记调用函数,尤其是当函数具有零参数或以暗示它可能是属性而不是函数的方式命名时.

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

// later...

// Broken code, do not use!
function doAdminThing(user: User) {
    // oops!
    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) {
    //  ~~~~~~~~~~~~~~~~~~~~
    // error! This condition will always return true since the function is always defined.
    //        Did you mean to call it instead?

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

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

function issueNotification(user: User) {
    if (user.doNotDisturb) {
        // OK, property is optional
    }
    if (user.notify) {
        // OK, called the function
        user.notify();
    }
}

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

我们非常感谢GitHub用户@jwbay ,他主动创建了概念验证并反复为我们提供了最新版本 .

// @ts-nocheck in TypeScript Files #

TypeScript 3.7允许我们在TypeScript文件的顶部添加// @ts-nocheck注释以禁用语义检查. 从历史上看,只有在checkJs存在的情况下,此注释才在JavaScript源文件中得到尊重,但我们已经扩展了对TypeScript文件的支持,以使所有用户的迁移更加容易.

Semicolon Formatter Option #

由于JavaScript的自动分号插入(ASI)规则,TypeScript的内置格式化程序现在支持在分号结尾可选的位置插入和删除分号. 该设置现在在Visual Studio Code Insiders中可用,在Visual Studio 16.4 Preview 2中的"工具选项"菜单中可用.

New semicolon formatter option in VS Code

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

3.7 Breaking Changes #

DOM Changes #

lib.dom.d.ts类型已更新 . 这些更改很大程度上是与可空性相关的正确性更改,但影响最终取决于您的代码库.

Class Field Mitigations #

如上所述 ,TypeScript 3.7在.d.ts文件中发出get / set访问器,这可能会导致消费者对3.5和.d.ts版本的TypeScript的旧版本进行重大更改. TypeScript 3.6用户将不会受到影响,因为该功能已针对该版本进行了过时验证.

虽然本身并不是破损,但在以下情况下,选择useDefineForClassFields标志可能会导致破损:

  • 用属性声明覆盖派生类中的访问器
  • 在没有初始化程序的情况下重新声明属性声明

要了解全部影响,请阅读上面有关useDefineForClassFields标志的部分 .

Function Truthy Checks #

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

  • 检查值来自可选属性
  • strictNullChecks已禁用
  • 该函数随后在if的正文中调用

Local and Imported Type Declarations Now Conflict #

由于存在错误,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); // Error! 'x' doesn't exist on 'SomeType'
}

在这里, SomeType似乎起源于import声明和本地interface声明. 也许令人惊讶的是,在模块内部, SomeType引用import定义,而局部声明SomeType仅在从另一个文件导入时才可用. 这非常令人困惑,并且我们对极少数这种情况下的代码案例的回顾表明,开发人员通常认为正在发生一些不同的事情.

在TypeScript 3.7中, 现在可以正确地将其标识为重复标识符error . 正确的解决方案取决于作者的初衷,并应逐案解决. 通常,命名冲突是无意的,最好的解决方法是重命名导入的类型. 如果要扩展导入的类型,则应编写适当的模块扩展.

3.7 API Changes #

为了启用上述递归类型别名模式,已从TypeReference接口中删除了typeArguments属性. 用户应该在TypeChecker实例上使用getTypeArguments函数.