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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索