可變元組型別(Variadic Tuple Types)
設想一個名為 concat 的 JavaScript 函式,它接收兩個陣列或元組型別,並將它們連線在一起以建立一個新陣列。
jsfunction concat(arr1, arr2) {return [...arr1, ...arr2];}
再考慮 tail 函式,它接收一個數組或元組,並返回除第一個元素外的所有元素。
jsfunction tail(arg) {const [_, ...result] = arg;return result;}
我們該如何在 TypeScript 中為這兩個函式編寫型別宣告?
對於 concat,在舊版本的語言中,唯一可行的方法是編寫多個過載。
tsfunction concat(arr1: [], arr2: []): [];function concat<A>(arr1: [A], arr2: []): [A];function concat<A, B>(arr1: [A, B], arr2: []): [A, B];function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];
呃……好吧,對於第二個陣列為空的情況,這已經需要七個過載了。讓我們再為 arr2 包含一個引數的情況新增一些。
tsfunction concat<A2>(arr1: [], arr2: [A2]): [A2];function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];
相信顯而易見,這已經變得極不合理了。不幸的是,在為 tail 這樣的函式編寫型別時,你最終也會遇到同樣的問題。
這是我們稱之為“千次過載導致的死亡”(death by a thousand overloads)的另一個案例,而且它甚至不能從根本上解決問題。它只能為你所寫的過載數量提供正確的型別。如果我們想建立一個萬能的包羅永珍的情況,我們需要像下面這樣的過載:
tsfunction concat<T, U>(arr1: T[], arr2: U[]): Array<T | U>;
但是,當使用元組時,該簽名並沒有編碼任何關於輸入長度或元素順序的資訊。
TypeScript 4.0 帶來了兩個根本性的變化,以及一些推斷改進,使得實現這些型別成為可能。
第一個變化是元組型別語法中的展開(spread)現在可以是泛型的。這意味著即使在我們不知道所操作的具體型別時,也可以表示元組和陣列的高階操作。當這些泛型展開在元組型別中被例項化(或被替換為真實型別)時,它們可以產生其他陣列和元組型別集合。
例如,這意味著我們可以為 tail 這樣的函式編寫型別,而無需陷入“千次過載”的困境。
tsTryfunctiontail <T extends any[]>(arr : readonly [any, ...T ]) {const [_ignored , ...rest ] =arr ;returnrest ;}constmyTuple = [1, 2, 3, 4] asconst ;constmyArray = ["hello", "world"];constr1 =tail (myTuple );constr2 =tail ([...myTuple , ...myArray ] asconst );
第二個變化是剩餘元素(rest elements)可以出現在元組的任何位置——而不僅僅是在末尾!
tstype Strings = [string, string];type Numbers = [number, number];type StrStrNumNumBool = [...Strings, ...Numbers, boolean];
以前,TypeScript 會發出如下錯誤:
A rest element must be last in a tuple type.
但在 TypeScript 4.0 中,這個限制被放寬了。
請注意,在我們展開一個長度未知的型別的情況下,結果型別也將變為非受限(unbounded),並且所有後續元素都會被計入結果的剩餘元素型別中。
tstype Strings = [string, string];type Numbers = number[];type Unbounded = [...Strings, ...Numbers, boolean];
透過結合這兩種行為,我們可以為 concat 編寫一個型別良好的單一簽名:
tsTrytypeArr = readonly any[];functionconcat <T extendsArr ,U extendsArr >(arr1 :T ,arr2 :U ): [...T , ...U ] {return [...arr1 , ...arr2 ];}
雖然那個簽名仍然有點長,但它只需要寫一次,不需要重複,並且在所有陣列和元組上都能提供可預測的行為。
這項功能本身就很棒,但在更復雜的場景中更是大放異彩。例如,考慮一個名為 partialCall 的偏函式應用函式。partialCall 接收一個函式——我們稱之為 f——以及 f 所期望的初始幾個引數。然後,它返回一個新的函式,該函式接收 f 仍然需要的任何其他引數,並在接收到它們時呼叫 f。
jsfunction partialCall(f, ...headArgs) {return (...tailArgs) => f(...headArgs, ...tailArgs);}
TypeScript 4.0 改進了剩餘引數和剩餘元組元素的推斷過程,以便我們能夠為它編寫型別,並讓它“自然而然地工作”。
tsTrytypeArr = readonly unknown[];functionpartialCall <T extendsArr ,U extendsArr ,R >(f : (...args : [...T , ...U ]) =>R ,...headArgs :T ) {return (...tailArgs :U ) =>f (...headArgs , ...tailArgs );}
在這種情況下,partialCall 能夠理解它最初可以或不可以接收哪些引數,並返回適當接收或拒絕剩餘內容的函式。
tsTryconstfoo = (x : string,y : number,z : boolean) => {};constArgument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.f1 =partialCall (foo ,100 );constExpected 4 arguments, but got 5.2554Expected 4 arguments, but got 5.f2 =partialCall (foo , "hello", 100, true,"oops" );// This works!constf3 =partialCall (foo , "hello");// What can we do with f3 now?// Works!f3 (123, true);Expected 2 arguments, but got 0.2554Expected 2 arguments, but got 0.(); f3 Argument of type 'string' is not assignable to parameter of type 'boolean'.2345Argument of type 'string' is not assignable to parameter of type 'boolean'.f3 (123,"hello" );
可變元組型別開啟了許多令人興奮的新模式,尤其是在函式組合方面。我們期望能夠利用它來更好地對 JavaScript 內建的 bind 方法進行型別檢查。為此,還加入了一些其他的推斷改進和模式,如果你有興趣瞭解更多,可以檢視關於可變元組的拉取請求。
帶標籤的元組元素(Labeled Tuple Elements)
改善圍繞元組型別和引數列表的體驗非常重要,因為它允許我們就常見的 JavaScript 慣用語(實際上就是對引數列表進行拆分和組合並傳遞給其他函式)獲得強型別驗證。我們能夠將元組型別用於剩餘引數,正是這一點至關重要的場景之一。
例如,以下使用元組型別作為剩餘引數的函式……
tsfunction foo(...args: [string, number]): void {// ...}
……對於 foo 的任何呼叫者來說,應該看起來與以下函式沒有區別……
tsfunction foo(arg0: string, arg1: number): void {// ...}
……
tsTryfoo ("hello", 42);Expected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.foo ("hello", 42,true );Expected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.("hello"); foo
然而,差異開始變得顯而易見的地方有一個:可讀性。在第一個示例中,我們沒有為第一個和第二個元素指定引數名稱。雖然這些對型別檢查沒有影響,但元組位置缺乏標籤會使它們更難使用——更難傳達我們的意圖。
這就是為什麼在 TypeScript 4.0 中,元組型別現在可以提供標籤。
tstype Range = [start: number, end: number];
為了加深引數列表和元組型別之間的聯絡,剩餘元素和可選元素的語法與引數列表的語法保持一致。
tstype Foo = [first: number, second?: string, ...rest: any[]];
使用帶標籤的元組有一些規則。首先,當標記元組的一個元素時,元組中的所有其他元素也必須被標記。
tsTrytypeBar = [first : string, number];
值得注意的是,標籤並不要求我們在解構時以不同方式命名變數。它們純粹是為了文件和工具支援而存在的。
tsTryfunctionfoo (x : [first : string,second : number]) {// ...// note: we didn't need to name these 'first' and 'second'const [a ,b ] =x ;a b }
總的來說,帶標籤的元組在利用元組和引數列表相關的模式,以及以型別安全的方式實現過載時非常方便。事實上,TypeScript 的編輯器支援將盡可能嘗試將它們顯示為過載。

要了解更多資訊,請檢視帶標籤元組元素的拉取請求。
從建構函式推斷類屬性
當啟用 noImplicitAny 時,TypeScript 4.0 現在可以使用控制流分析來確定類中屬性的型別。
tsTryclassSquare {// Previously both of these were anyarea ;sideLength ;constructor(sideLength : number) {this.sideLength =sideLength ;this.area =sideLength ** 2;}}
在建構函式的路徑沒有全部為例項成員賦值的情況下,該屬性被認為可能為 undefined。
tsTryclassSquare {sideLength ;constructor(sideLength : number) {if (Math .random ()) {this.sideLength =sideLength ;}}getarea () {return this.Object is possibly 'undefined'.2532Object is possibly 'undefined'.sideLength ** 2;}}
在你知道具體情況(例如,你有一個某種型別的 initialize 方法)的情況下,如果你開啟了 strictPropertyInitialization,你仍然需要一個明確的型別註解以及一個明確的賦值斷言(!)。
tsTryclassSquare {// definite assignment assertion// vsideLength !: number;// type annotationconstructor(sideLength : number) {this.initialize (sideLength );}initialize (sideLength : number) {this.sideLength =sideLength ;}getarea () {return this.sideLength ** 2;}}
更多詳細資訊,請參閱實現的拉取請求。
短路賦值運算子
JavaScript 和許多其他語言支援一組稱為複合賦值運算子的運算子。複合賦值運算子將運算子應用於兩個引數,然後將結果賦值給左側。你可能之前見過這些:
ts// Addition// a = a + ba += b;// Subtraction// a = a - ba -= b;// Multiplication// a = a * ba *= b;// Division// a = a / ba /= b;// Exponentiation// a = a ** ba **= b;// Left Bit Shift// a = a << ba <<= b;
JavaScript 中的許多運算子都有相應的賦值運算子!然而直到最近,還有三個顯著的例外:邏輯與(&&)、邏輯或(||)和空值合併(??)。
這就是為什麼 TypeScript 4.0 支援一項新的 ECMAScript 功能,增加了三個新的賦值運算子:&&=、||= 和 ??=。
這些運算子非常適合替代使用者可能編寫如下程式碼的任何示例:
tsa = a && b;a = a || b;a = a ?? b;
或者類似的 if 塊:
ts// could be 'a ||= b'if (!a) {a = b;}
我們甚至看到過(或者,呃,自己寫過)一些模式來延遲初始化值,僅在需要時才進行。
tslet values: string[];(values ?? (values = [])).push("hello");// After(values ??= []).push("hello");
(看,我們對寫出的所有程式碼並不都感到驕傲……)
在極少數情況下,如果你使用帶有副作用的 getter 或 setter,值得注意的是,這些運算子僅在必要時才執行賦值。從這個意義上講,不僅運算子的右側是“短路”的——賦值本身也是如此。
tsobj.prop ||= foo();// roughly equivalent to either of the followingobj.prop || (obj.prop = foo());if (!obj.prop) {obj.prop = foo();}
嘗試執行以下示例,看看這與始終執行賦值有何不同。
tsTryconstobj = {getprop () {console .log ("getter has run");// Replace me!returnMath .random () < 0.5;},setprop (_val : boolean) {console .log ("setter has run");}};functionfoo () {console .log ("right side evaluated");return true;}console .log ("This one always runs the setter");obj .prop =obj .prop ||foo ();console .log ("This one *sometimes* runs the setter");obj .prop ||=foo ();
我們要向社群成員 Wenlu Wang 表示衷心的感謝,感謝他的貢獻!
有關更多詳細資訊,你可以在這裡檢視拉取請求。你也可以檢視此功能的 TC39 提案倉庫。
catch 子句繫結上的 unknown
自 TypeScript 誕生之初,catch 子句變數就被定義為 any 型別。這意味著 TypeScript 允許你對它們做任何你想做的事情。
tsTrytry {// Do some work} catch (x ) {// x has type 'any' - have fun!console .log (x .message );console .log (x .toUpperCase ());x ++;x .yadda .yadda .yadda ();}
如果我們試圖防止在錯誤處理程式碼中出現更多錯誤,上述情況會有一些不良行為!因為這些變數預設具有 any 型別,它們缺乏任何型別安全,無法在無效操作上報錯。
這就是為什麼 TypeScript 4.0 現在允許你將 catch 子句變數的型別指定為 unknown。unknown 比 any 更安全,因為它提醒我們在操作值之前需要進行某種型別檢查。
tsTrytry {// ...} catch (e : unknown) {// Can't access values on unknowns'e' is of type 'unknown'.18046'e' is of type 'unknown'.console .log (. e toUpperCase ());if (typeofe === "string") {// We've narrowed 'e' down to the type 'string'.console .log (e .toUpperCase ());}}
雖然 catch 變數的型別預設不會改變,但我們將來可能會考慮一個新的 strict 模式標誌,以便使用者可以選擇此行為。在此期間,編寫 lint 規則來強制 catch 變數具有 : any 或 : unknown 的顯式註解是可能的。
有關更多詳細資訊,你可以預覽此功能的更改。
自定義 JSX 工廠
在使用 JSX 時,片段(fragment)是一種允許我們返回多個子元素的 JSX 元素。當我們第一次在 TypeScript 中實現片段時,我們並不清楚其他庫將如何利用它們。如今,大多數鼓勵使用 JSX 並支援片段的其他庫都有類似的 API 結構。
在 TypeScript 4.0 中,使用者可以透過新的 jsxFragmentFactory 選項自定義片段工廠。
作為一個例子,以下 tsconfig.json 檔案告訴 TypeScript 以與 React 相容的方式轉換 JSX,但將每個工廠呼叫切換為 h 而不是 React.createElement,並使用 Fragment 而不是 React.Fragment。
{"": {"": "esnext","": "commonjs","": "react","": "h","": "Fragment"}}
在需要為每個檔案指定不同 JSX 工廠的情況下,你可以利用新的 /** @jsxFrag */ pragma 註釋。例如,以下程式碼……
tsxTry// Note: these pragma comments need to be written// with a JSDoc-style multiline syntax to take effect./** @jsx h *//** @jsxFrag Fragment */import {h ,Fragment } from "preact";export constHeader = (<><h1 >Welcome</h1 ></>);
……將被轉換為如下輸出的 JavaScript……
tsxTryimport React from 'react';export const Header = (React.createElement(React.Fragment, null,React.createElement("h1", null, "Welcome")));
我們要向社群成員 Noj Vek 表示衷心的感謝,感謝他提交此拉取請求並耐心地與我們的團隊合作。
你可以檢視拉取請求以瞭解更多詳細資訊!
在帶有 --noEmitOnError 的 build 模式下的速度提升
以前,在使用 noEmitOnError 標誌時,在先前有錯誤的編譯之後再次編譯程式會非常慢。這是因為上一次編譯中的任何資訊都不會基於 noEmitOnError 標誌快取到 .tsbuildinfo 檔案中。
TypeScript 4.0 改變了這一點,在這些場景中帶來了巨大的速度提升,進而改進了 --build 模式場景(這意味著同時啟用了 incremental 和 noEmitOnError)。
有關詳細資訊,請閱讀拉取請求中的更多資訊。
帶有 --noEmit 的 --incremental
TypeScript 4.0 允許我們在使用 noEmit 標誌的同時利用 incremental 編譯。這在以前是不允許的,因為 incremental 需要生成 .tsbuildinfo 檔案;然而,啟用更快的增量構建的使用場景對於所有使用者來說都非常重要。
有關更多詳細資訊,你可以參閱實現的拉取請求。
編輯器改進
TypeScript 編譯器不僅為大多數主要編輯器中 TypeScript 本身的編輯體驗提供支援,它還為 Visual Studio 系列編輯器及其他編輯器中的 JavaScript 體驗提供支援。因此,我們的大部分工作集中在改進編輯器場景上——這是作為開發者你花費時間最多的地方。
在編輯器中使用新的 TypeScript/JavaScript 功能的方式會因編輯器而異,但
- Visual Studio Code 支援選擇不同版本的 TypeScript。此外,還有 JavaScript/TypeScript Nightly 擴充套件以保持在最前沿(這通常非常穩定)。
- Visual Studio 2017/2019 擁有[上述 SDK 安裝程式]和 MSBuild 安裝。
- Sublime Text 3 支援選擇不同版本的 TypeScript。
你可以檢視支援 TypeScript 的編輯器部分列表,以瞭解更多關於你最喜歡的編輯器是否支援使用新版本的資訊。
轉換為可選鏈
可選鏈是一項最近受到廣泛歡迎的功能。這就是為什麼 TypeScript 4.0 引入了新的重構功能,以將常見模式轉換為利用可選鏈和空值合併!

請記住,雖然由於 JavaScript 中真值/假值的微妙之處,這種重構並不完全捕捉相同的行為,但我們認為它應該捕捉大多數使用場景的意圖,特別是當 TypeScript 對你的型別有更精確的瞭解時。
有關更多詳細資訊,請檢視此功能的拉取請求。
/** @deprecated */ 支援
TypeScript 的編輯支援現在能夠識別宣告何時被標記為 /** @deprecated */ JSDoc 註釋。該資訊會顯示在補全列表中,並作為編輯器可以專門處理的建議診斷。在 VS Code 等編輯器中,棄用的值通常以刪除線樣式顯示,就像這樣。

此新功能得益於 Wenlu Wang。有關更多詳細資訊,請參閱拉取請求。
啟動時的部分語義模式
我們聽到了許多關於啟動時間過長的使用者抱怨,特別是在大型專案中。罪魁禍首通常是一個稱為程式構建的過程。這是從一組初始根檔案開始,解析它們,查詢它們的依賴項,解析這些依賴項,查詢這些依賴項的依賴項,依此類推的過程。專案越大,你等待基本編輯器操作(如轉到定義或快速資訊)的時間就越長。
這就是為什麼我們一直在為編輯器開發一種新模式,以便在完整的語言服務體驗載入之前提供部分體驗。核心思想是編輯器可以執行一個輕量級的部分伺服器,該伺服器僅檢視編輯器當前開啟的檔案。
很難確切說你會看到什麼樣的改進,但據瞭解,TypeScript 在 Visual Studio Code 程式碼庫上完全響應之前,通常需要20 秒到 1 分鐘。相比之下,我們新的部分語義模式似乎將該延遲降低到了僅幾秒鐘。作為一個例子,在下面的影片中,你可以看到兩個並排的編輯器,左側執行的是 TypeScript 3.9,右側執行的是 TypeScript 4.0。
當在特別大的程式碼庫上同時重啟兩個編輯器時,帶有 TypeScript 3.9 的編輯器根本無法提供補全或快速資訊。另一方面,帶有 TypeScript 4.0 的編輯器可以立即為我們正在編輯的當前檔案提供豐富的體驗,儘管它在後臺載入完整的專案。
目前唯一支援此模式的編輯器是 Visual Studio Code,它在 Visual Studio Code Insiders 中會有一些 UX 改進。我們認識到這種體驗在 UX 和功能上仍有提升空間,並且我們有一個改進列表。我們正在尋求更多關於你認為可能有什麼用處的反饋。
有關更多資訊,你可以參閱原始提案,實現的拉取請求,以及後續的元問題。
更智慧的自動匯入
自動匯入是一項了不起的功能,它使編碼變得更加容易;然而,每當自動匯入似乎不起作用時,它都會讓使用者非常困惑。我們從使用者那裡聽到的一個具體問題是,自動匯入在用 TypeScript 編寫的依賴項上不起作用——也就是說,直到他們在專案的其他地方編寫了至少一個顯式的匯入。
為什麼自動匯入適用於 @types 包,但不適用於提供自己型別的包?事實證明,自動匯入僅適用於你的專案已經包含的包。由於 TypeScript 有一些古怪的預設設定,會自動將 node_modules/@types 中的包新增到你的專案中,所以那些包會被自動匯入。另一方面,其他包被排除在外,因為遍歷你所有的 node_modules 包可能非常昂貴。
所有這些導致了當你試圖自動匯入剛剛安裝但尚未使用的東西時,入門體驗非常糟糕。
TypeScript 4.0 現在在編輯器場景中做了一些額外的工作,以包含你在 package.json 的 dependencies(和 peerDependencies)欄位中列出的包。這些包的資訊僅用於改進自動匯入,不會改變型別檢查等任何其他內容。這允許我們為所有具有型別的依賴項提供自動匯入,而無需承擔完整的 node_modules 搜尋成本。
在極少數情況下,如果你的 package.json 列出了超過 10 個尚未匯入的型別化依賴項,此功能會自動停用自身以防止專案載入變慢。要強制該功能工作,或完全停用它,你應該能夠配置你的編輯器。對於 Visual Studio Code,這是“Include Package JSON Auto Imports”(或 typescript.preferences.includePackageJsonAutoImports)設定。
我們的新網站!
TypeScript 網站最近已經從頭開始重寫並推出!

我們已經寫了一些關於我們新網站的內容,所以你可以去那裡閱讀更多資訊;但值得一提的是,我們仍在期待你的想法!如果你有任何問題、意見或建議,你可以在網站的問題追蹤器上提出。
破壞性變更
lib.d.ts 變更
我們的 lib.d.ts 宣告已經改變——最具體地說,DOM 的型別已經改變。最顯著的變化可能是刪除了document.origin,它僅在舊版本的 IE 和 Safari 中有效,MDN 建議改用 self.origin。
屬性重寫訪問器(反之亦然)是一個錯誤
以前,只有在使用 useDefineForClassFields 時,屬性重寫訪問器或訪問器重寫屬性才是一個錯誤;然而,TypeScript 現在在派生類中宣告會重寫基類中的 getter 或 setter 的屬性時,總是會發出錯誤。
tsTryclassBase {getfoo () {return 100;}setfoo (value ) {// ...}}classDerived extendsBase {'foo' is defined as an accessor in class 'Base', but is overridden here in 'Derived' as an instance property.2610'foo' is defined as an accessor in class 'Base', but is overridden here in 'Derived' as an instance property.= 10; foo }
tsTryclassBase {prop = 10;}classDerived extendsBase {get'prop' is defined as a property in class 'Base', but is overridden here in 'Derived' as an accessor.2611'prop' is defined as a property in class 'Base', but is overridden here in 'Derived' as an accessor.() { prop return 100;}}
有關更多詳細資訊,請參見實現的拉取請求。
delete 的運算元必須是可選的
在使用 strictNullChecks 時,使用 delete 運算子的運算元現在必須是 any、unknown、never,或者是可選的(即型別中包含 undefined)。否則,使用 delete 運算子將是一個錯誤。
tsTryinterfaceThing {prop : string;}functionf (x :Thing ) {deleteThe operand of a 'delete' operator must be optional.2790The operand of a 'delete' operator must be optional.x .prop ;}
有關更多詳細資訊,請參見實現的拉取請求。
棄用 TypeScript 的節點工廠(Node Factory)的使用
今天,TypeScript 提供了一組用於生成 AST 節點的“工廠”函式;然而,TypeScript 4.0 提供了一個新的節點工廠 API。因此,對於 TypeScript 4.0,我們決定棄用這些舊的函式,轉而支援新的函式。
有關更多詳細資訊,請閱讀有關此更改的相關拉取請求。
有關更多詳細資訊,你可以檢視