改進的交集縮減、聯合型別相容性與型別收窄
TypeScript 4.8 在 --strictNullChecks 模式下帶來了一系列正確性和一致性的改進。這些變化影響了交集型別和聯合型別的工作方式,並被利用於 TypeScript 的型別收窄機制中。
例如,unknown 在理念上非常接近聯合型別 {} | null | undefined,因為它接受 null、undefined 和任何其他型別。TypeScript 現在能夠識別這一點,並允許將 unknown 賦值給 {} | null | undefined。
tsfunction f(x: unknown, y: {} | null | undefined) {x = y; // always workedy = x; // used to error, now works}
另一個變化是,{} 與任何其他物件型別的交集現在會直接簡化為該物件型別。這意味著我們可以重寫 NonNullable,使其僅使用與 {} 的交集,因為 {} & null 和 {} & undefined 會直接被排除。
diff- type NonNullable<T> = T extends null | undefined ? never : T;+ type NonNullable<T> = T & {};
這是一個改進,因為這種交集型別可以被縮減並賦值,而條件型別目前無法做到。因此,NonNullable<NonNullable<T>> 現在至少可以簡化為 NonNullable<T>,而之前是不行的。
tsfunction foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {x = y; // always workedy = x; // used to error, now works}
這些變化還使我們能夠在控制流分析和型別收窄方面做出合理的改進。例如,在真值分支中,unknown 現在可以像 {} | null | undefined 一樣被收窄。
tsfunction narrowUnknownishUnion(x: {} | null | undefined) {if (x) {x; // {}}else {x; // {} | null | undefined}}function narrowUnknown(x: unknown) {if (x) {x; // used to be 'unknown', now '{}'}else {x; // unknown}}
泛型值也會得到類似的收窄。當檢查某個值不為 null 或 undefined 時,TypeScript 現在只是將其與 {} 相交——這同樣等同於說它是 NonNullable。將這裡的許多變化結合起來,我們現在可以在沒有任何型別斷言的情況下定義以下函式。
tsfunction throwIfNullable<T>(value: T): NonNullable<T> {if (value === undefined || value === null) {throw Error("Nullable value!");}// Used to fail because 'T' was not assignable to 'NonNullable<T>'.// Now narrows to 'T & {}' and succeeds because that's just 'NonNullable<T>'.return value;}
value 現在被收窄為 T & {},這與 NonNullable<T> 完全相同——因此函式體無需任何 TypeScript 特有的語法即可正常工作。
這些變化本身看起來可能很小,但它們修復了多年來報告的許多細小痛點。
關於這些改進的更多細節,您可以在此處閱讀更多資訊。
模板字串型別中 infer 型別推斷的改進
TypeScript 最近引入了一種為條件型別中的 infer 型別變數新增 extends 約束的方法。
ts// Grabs the first element of a tuple if it's assignable to 'number',// and returns 'never' if it can't find one.type TryGetNumberIfFirst<T> =T extends [infer U extends number, ...unknown[]] ? U : never;
如果這些 infer 型別出現在模板字串型別中並被約束為基本型別,TypeScript 現在將嘗試解析出字面量型別。
ts// SomeNum used to be 'number'; now it's '100'.type SomeNum = "100" extends `${infer U extends number}` ? U : never;// SomeBigInt used to be 'bigint'; now it's '100n'.type SomeBigInt = "100" extends `${infer U extends bigint}` ? U : never;// SomeBool used to be 'boolean'; now it's 'true'.type SomeBool = "true" extends `${infer U extends boolean}` ? U : never;
這可以更好地傳達庫在執行時的行為,並提供更精確的型別。
需要注意的是,當 TypeScript 解析這些字面量型別時,它會貪婪地嘗試解析出看起來像適當基本型別的儘可能多的內容;然而,它隨後會檢查該基本型別的反向列印是否與原始字串內容相匹配。換句話說,TypeScript 會檢查從字串到基本型別再回到字串的過程是否能“往返”成功。如果它發現該字串無法“往返”,則會回退到基礎的基本型別。
ts// JustNumber is `number` here because TypeScript parses out `"1.0"`, but `String(Number("1.0"))` is `"1"` and doesn't match.type JustNumber = "1.0" extends `${infer T extends number}` ? T : never;
您可以在此處檢視有關此功能的更多資訊。
--build、--watch 和 --incremental 的效能改進
TypeScript 4.8 引入了幾項最佳化,旨在加快 --watch 和 --incremental 場景下的速度,以及使用 --build 進行的專案引用構建。例如,TypeScript 現在能夠在 --watch 模式下出現空操作更改時避免更新時間戳,這使得重新構建更快,並避免了干擾可能正在監視 TypeScript 輸出的其他構建工具。此外,還引入了許多其他最佳化,使我們能夠在 --build、--watch 和 --incremental 之間重用資訊。
這些改進有多大?在相當龐大的內部程式碼庫中,我們發現許多簡單常見操作的時間減少了約 10%-25%,而在無更改場景下時間減少了約 40%。我們在 TypeScript 程式碼庫上也看到了類似的結果。
您可以在 GitHub 上檢視這些更改以及效能結果。
比較物件和陣列字面量時的錯誤
在許多語言中,像 == 這樣的運算子會對物件執行所謂的“值”相等性檢查。例如,在 Python 中,透過使用 == 檢查值是否等於空列表來判斷列表是否為空是合法的。
pyif people_at_home == []:print("here's where I lie, broken inside. </3")adopt_animals()
但在 JavaScript 中並非如此,物件(因此也包括陣列)之間的 == 和 === 檢查的是兩個引用是否指向同一個值。我們認為 JavaScript 中類似的程式碼充其量是 JavaScript 開發者的陷阱,最壞的情況下是生產程式碼中的錯誤。這就是為什麼 TypeScript 現在禁止使用類似以下的程式碼。
tsif (peopleAtHome === []) {// ~~~~~~~~~~~~~~~~~~~// This condition will always return 'false' since JavaScript compares objects by reference, not value.console.log("here's where I lie, broken inside. </3")adoptAnimals();}
我們要感謝 Jack Works 貢獻了此檢查。您可以在此處檢視相關更改。
改進了繫結模式的推斷
在某些情況下,TypeScript 會從繫結模式中獲取型別以進行更好的推斷。
tsdeclare function chooseRandomly<T>(x: T, y: T): T;let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]);// ^ ^ ^// | | |// | | string// | |// | boolean// |// number
當 chooseRandomly 需要計算 T 的型別時,它主要會檢視 [42, true, "hi!"] 和 [0, false, "bye!"];但 TypeScript 需要確定這兩個型別應該是 Array<number | boolean | string> 還是元組型別 [number, boolean, string]。為此,它會查詢現有的候選項作為提示,看是否存在任何元組型別。當 TypeScript 看到繫結模式 [a, b, c] 時,它會建立型別 [any, any, any],該型別被選為 T 的低優先順序候選項,並被用作 [42, true, "hi!"] 和 [0, false, "bye!"] 型別計算的提示。
您可以看到這對於 chooseRandomly 有何幫助,但在其他情況下則不然。例如,請看以下程式碼
tsdeclare function f<T>(x?: T): T;let [x, y, z] = f();
繫結模式 [x, y, z] 暗示 f 應該產生一個 [any, any, any] 元組;但 f 其實不應該根據繫結模式更改其型別引數。它無法根據被賦值的物件突然變出一個新的類陣列值,因此繫結模式型別對所產生的型別影響太大。此外,由於繫結模式型別充滿了 any,導致 x、y 和 z 被標記為 any。
在 TypeScript 4.8 中,這些繫結模式不再被用作型別引數的候選項。相反,它們僅在引數需要更具體型別時才會被參考,就像我們的 chooseRandomly 示例中那樣。如果您需要恢復舊行為,可以隨時提供顯式的型別引數。
如果您想了解更多資訊,可以檢視 GitHub 上的更改。
檔案監視修復(特別是在 git checkout 切換分支時)
我們有一個長期存在的 Bug,導致 TypeScript 在 --watch 模式和編輯器場景中處理某些檔案更改時非常吃力。有時症狀是出現陳舊或不準確的錯誤,需要重啟 tsc 或 VS Code。這種情況經常發生在 Unix 系統上,您可能在儲存 vim 檔案或在 git 中切換分支後遇到過這種情況。
這是由 Node.js 如何跨檔案系統處理重新命名事件的假設引起的。Linux 和 macOS 使用的檔案系統利用了 inode,而 Node.js 會將檔案監視器附加到 inode 而不是檔案路徑。因此,當 Node.js 返回 一個監視器物件 時,它可能根據平臺和檔案系統監視的是路徑或 inode。
為了提高效率,TypeScript 會在檢測到路徑在磁碟上仍然存在時嘗試重用同一個監視器物件。這就是出錯的地方,因為即使該路徑上仍然存在檔案,也可能建立了一個不同的檔案,而該檔案將具有不同的 inode。因此,TypeScript 會最終重用監視器物件,而不是在原始位置安裝新監視器,導致去監視一個完全不相關的檔案。TypeScript 4.8 現在在 inode 系統上正確處理這些情況,安裝新的監視器並修復了此問題。
我們要感謝 Marc Celani 和他 Airtable 的團隊,他們投入了大量時間調查所遇到的問題並指出了根本原因。您可以在此處檢視檔案監視的具體修復。
“查詢所有引用”效能改進
當您在編輯器中執行“查詢所有引用”時,TypeScript 現在可以在聚合引用時表現得更聰明。這使得 TypeScript 在其自身程式碼庫中搜索廣泛使用的識別符號所花費的時間減少了約 20%。
從自動匯入中排除特定檔案
TypeScript 4.8 引入了一個編輯器首選項,用於從自動匯入中排除檔案。在 Visual Studio Code 中,可以在設定 UI 的“自動匯入排除檔案模式(Auto Import File Exclude Patterns)”下新增檔名或 glob 模式,或者在 .vscode/settings.json 檔案中進行配置。
jsonc{// Note that `javascript.preferences.autoImportFileExcludePatterns` can be specified for JavaScript too."typescript.preferences.autoImportFileExcludePatterns": ["**/node_modules/@types/node"]}
這在您無法避免在編譯中包含某些模組或庫,但又很少想從中匯入內容的情況下非常有用。這些模組可能有許多匯出內容,這些內容可能會汙染自動匯入列表並使導航變得困難,此選項可以在這些情況下提供幫助。
您可以在此處檢視有關實現的更多細節。
正確性修復和破壞性更改
由於型別系統更改的性質,很難進行不影響某些程式碼的更改;但是,有少數更改可能需要調整現有程式碼。
lib.d.ts 更新
雖然 TypeScript 努力避免重大破壞,但即使是內建庫中的微小更改也可能導致問題。我們預計 DOM 和 lib.d.ts 更新不會導致重大破壞,但一個值得注意的更改是,Error 上的 cause 屬性現在具有型別 unknown 而不是 Error。
無約束泛型不再可賦值給 {}
在 TypeScript 4.8 中,對於啟用了 strictNullChecks 的專案,如果無約束型別引數被用於不允許 null 或 undefined 的位置,TypeScript 現在會正確發出錯誤。這包括任何期望 {}、object 或所有屬性都是可選的物件型別的型別。
一個簡單的例子如下。
ts// Accepts any non-null non-undefined valuefunction bar(value: {}) {Object.keys(value); // This call throws on null/undefined at runtime.}// Unconstrained type parameter T...function foo<T>(x: T) {bar(x); // Used to be allowed, now is an error in 4.8.// ~// error: Argument of type 'T' is not assignable to parameter of type '{}'.}foo(undefined);
如上所示,這樣的程式碼有潛在的 Bug——值 null 和 undefined 可以透過這些無約束的型別引數間接傳遞給不應該觀察到這些值的程式碼。
這種行為也會在型別位置可見。一個例子是
tsinterface Foo<T> {x: Bar<T>;}interface Bar<T extends {}> { }
不想處理 null 和 undefined 的現有程式碼可以透過傳播適當的約束來修復。
diff- function foo<T>(x: T) {+ function foo<T extends {}>(x: T) {
另一個解決方法是在執行時檢查 null 和 undefined。
difffunction foo<T>(x: T) {+ if (x !== null && x !== undefined) {bar(x);+ }}
如果您知道由於某種原因您的泛型值不可能是 null 或 undefined,您可以使用非空斷言。
difffunction foo<T>(x: T) {- bar(x);+ bar(x!);}
在型別方面,您通常要麼需要傳播約束,要麼將您的型別與 {} 相交。
有關更多資訊,您可以檢視引入此更改的 PR 以及關於無約束泛型如何工作的工作討論議題。
裝飾器現在放置在 TypeScript 語法樹的 modifiers 欄位中
TC39 中裝飾器的當前發展方向意味著 TypeScript 將不得不處理裝飾器放置方面的破壞性更改。之前,TypeScript 假設裝飾器總是放置在所有關鍵字/修飾符之前。例如
ts@decoratorexport class Foo {// ...}
當前提出的裝飾器不支援此語法。相反,export 關鍵字必須位於裝飾器之前。
tsexport @decorator class Foo {// ...}
不幸的是,TypeScript 的樹是具體而非抽象的,並且我們的架構期望語法樹節點欄位完全有序地排列。為了同時支援遺留裝飾器和擬議中的裝飾器,TypeScript 必須優雅地解析並交織修飾符和裝飾器。
為此,它暴露了一個新的類型別名 ModifierLike,它是 Modifier 或 Decorator。
tsexport type ModifierLike = Modifier | Decorator;
裝飾器現在與 modifiers 放在同一個欄位中,該欄位現在在設定時為 NodeArray<ModifierLike>,並且整個舊欄位已棄用。
diff- readonly modifiers?: NodeArray<Modifier> | undefined;+ /**+ * @deprecated ...+ * Use `ts.canHaveModifiers()` to test whether a `Node` can have modifiers.+ * Use `ts.getModifiers()` to get the modifiers of a `Node`.+ * ...+ */+ readonly modifiers?: NodeArray<ModifierLike> | undefined;
所有現有的 decorators 屬性已被標記為已棄用,讀取時將始終為 undefined。其型別也已更改為 undefined,以便現有工具知道如何正確處理它們。
diff- readonly decorators?: NodeArray<Decorator> | undefined;+ /**+ * @deprecated ...+ * Use `ts.canHaveDecorators()` to test whether a `Node` can have decorators.+ * Use `ts.getDecorators()` to get the decorators of a `Node`.+ * ...+ */+ readonly decorators?: undefined;
為了避免新的棄用警告和其他問題,TypeScript 現在暴露了四個新函式來代替 decorators 和 modifiers 屬性。其中包括用於測試節點是否支援修飾符和裝飾器的謂詞,以及用於獲取它們的相應訪問器函式。
tsfunction canHaveModifiers(node: Node): node is HasModifiers;function getModifiers(node: HasModifiers): readonly Modifier[] | undefined;function canHaveDecorators(node: Node): node is HasDecorators;function getDecorators(node: HasDecorators): readonly Decorator[] | undefined;
作為一個如何獲取節點修飾符的示例,您可以編寫
tsconst modifiers = canHaveModifiers(myNode) ? getModifiers(myNode) : undefined;
注意,每次呼叫 getModifiers 和 getDecorators 可能會分配一個新陣列。
有關更多資訊,請參閱以下更改:
型別不能在 JavaScript 檔案中匯入/匯出
TypeScript 之前允許 JavaScript 檔案在 import 和 export 語句中匯入和匯出僅宣告為型別而不包含值的實體。此行為是不正確的,因為在 ECMAScript 模組下,對不存在的值進行命名匯入和匯出將導致執行時錯誤。當在 --checkJs 或透過 // @ts-check 註釋對 JavaScript 檔案進行型別檢查時,TypeScript 現在會發出錯誤。
ts// @ts-check// Will fail at runtime because 'SomeType' is not a value.import { someValue, SomeType } from "some-module";/*** @type {SomeType}*/export const myValue = someValue;/*** @typedef {string | number} MyType*/// Will fail at runtime because 'MyType' is not a value.export { MyType as MyExportedType };
要引用另一個模組中的型別,您可以直接限定匯入。
diff- import { someValue, SomeType } from "some-module";+ import { someValue } from "some-module";/**- * @type {SomeType}+ * @type {import("some-module").SomeType}*/export const myValue = someValue;
要匯出型別,您可以直接在 JSDoc 中使用 /** @typedef */ 註釋。@typedef 註釋會自動匯出其所在模組中的型別。
diff/*** @typedef {string | number} MyType*/+ /**+ * @typedef {MyType} MyExportedType+ */- export { MyType as MyExportedType };
您可以在此處閱讀有關此更改的更多資訊。
繫結模式不再直接貢獻推斷候選項
如上所述,繫結模式不再更改函式呼叫中推斷結果的型別。您可以在此處閱讀更多有關原始更改的資訊。
繫結模式中未使用的重新命名現在是型別簽名中的錯誤
TypeScript 的型別註解語法看起來經常可以在解構值時使用。例如,請看以下函式。
tsdeclare function makePerson({ name: string, age: number }): Person;
您可能會閱讀此簽名並認為 makePerson 顯然接受一個具有型別為 string 的 name 屬性和型別為 number 的 age 屬性的物件;然而,JavaScript 的解構語法實際上在這裡佔了上風。makePerson 確實聲稱它將接受一個具有 name 和 age 屬性的物件,但它沒有為它們指定型別,而是在說它將 name 和 age 分別重新命名為 string 和 number。
在純型別構造中,編寫這樣的程式碼是沒有用的,而且通常是一個錯誤,因為開發者通常認為他們正在編寫型別註解。
TypeScript 4.8 將這些設為錯誤,除非它們在簽名中稍後被引用。編寫上述簽名的正確方法如下
tsdeclare function makePerson(options: { name: string, age: number }): Person;// ordeclare function makePerson({ name, age }: { name: string, age: number }): Person;
此更改可以捕獲宣告中的錯誤,並有助於改進現有程式碼。我們要感謝 GitHub 使用者 uhyo 提供此檢查。您可以閱讀有關此更改的資訊。