僅型別匯入和匯出 (Type-Only Imports and Export)
對於大多數使用者來說,這個特性可能永遠用不上;但是,如果你在使用 isolatedModules、TypeScript 的 transpileModule API 或 Babel 時遇到過問題,那麼這個特性可能與你有關。
TypeScript 3.8 為僅型別匯入和匯出添加了新的語法。
tsimport type { SomeThing } from "./some-module.js";export type { SomeThing };
import type 僅匯入用於型別註解和宣告的宣告。它始終會被完全擦除,因此在執行時不會留下任何痕跡。同樣,export type 也僅提供可用於型別上下文的匯出,並從 TypeScript 的輸出中擦除。
需要注意的是,類在執行時具有值,在設計時具有型別,且其用途是上下文敏感的。當使用 import type 匯入類時,你不能進行繼承等操作。
tsimport type { Component } from "react";interface ButtonProps {// ...}class Button extends Component<ButtonProps> {// ~~~~~~~~~// error! 'Component' only refers to a type, but is being used as a value here.// ...}
如果你之前使用過 Flow,那麼語法非常相似。不同之處在於我們增加了一些限制,以避免出現可能存在歧義的程式碼。
ts// Is only 'Foo' a type? Or every declaration in the import?// We just give an error because it's not clear.import type Foo, { Bar, Baz } from "some-module";// ~~~~~~~~~~~~~~~~~~~~~~// error! A type-only import can specify a default import or named bindings, but not both.
除了 import type,TypeScript 3.8 還添加了一個新的編譯器標誌來控制執行時未使用的匯入:importsNotUsedAsValues。該標誌接受 3 種不同的值:
remove:這是目前的行為,即丟棄這些匯入。它將繼續作為預設值,並且是非破壞性更改。preserve:這會保留所有值未被使用的匯入。這可能導致匯入/副作用被保留。error:這會保留所有匯入(與preserve選項相同),但當值匯入僅用作型別時會報錯。如果你想確保沒有意外匯入值,但仍希望顯式保留副作用匯入,這可能很有用。
有關此功能的更多資訊,你可以檢視 pull request,以及關於擴充套件 import type 宣告來源使用範圍的相關更改。
ECMAScript 私有欄位 (Private Fields)
TypeScript 3.8 帶來了對 ECMAScript 私有欄位的支援,這是 Stage-3 類欄位提案的一部分。
tsclass Person {#name: string;constructor(name: string) {this.#name = name;}greet() {console.log(`Hello, my name is ${this.#name}!`);}}let jeremy = new Person("Jeremy Bearimy");jeremy.#name;// ~~~~~// Property '#name' is not accessible outside class 'Person'// because it has a private identifier.
與普通屬性(即使是使用 private 修飾符宣告的屬性)不同,私有欄位有幾條規則需要記住。其中一些是:
- 私有欄位以
#字元開頭。有時我們稱這些為私有名稱。 - 每個私有欄位名稱的作用域唯一地限定在包含它的類中。
- TypeScript 的可訪問性修飾符(如
public或private)不能用於私有欄位。 - 私有欄位在包含類之外無法被訪問甚至無法被檢測到——即使是對 JS 使用者也是如此!有時我們稱之為硬隱私。
除了“硬”隱私外,私有欄位的另一個好處是我們剛才提到的唯一性。例如,普通屬性宣告在子類中很容易被覆蓋。
tsclass C {foo = 10;cHelper() {return this.foo;}}class D extends C {foo = 20;dHelper() {return this.foo;}}let instance = new D();// 'this.foo' refers to the same property on each instance.console.log(instance.cHelper()); // prints '20'console.log(instance.dHelper()); // prints '20'
使用私有欄位,你永遠不必擔心這一點,因為每個欄位名稱對於包含它的類來說都是唯一的。
tsclass C {#foo = 10;cHelper() {return this.#foo;}}class D extends C {#foo = 20;dHelper() {return this.#foo;}}let instance = new D();// 'this.#foo' refers to a different field within each class.console.log(instance.cHelper()); // prints '10'console.log(instance.dHelper()); // prints '20'
值得注意的另一點是,在任何其他型別上訪問私有欄位會導致 TypeError!
tsclass Square {#sideLength: number;constructor(sideLength: number) {this.#sideLength = sideLength;}equals(other: any) {return this.#sideLength === other.#sideLength;}}const a = new Square(100);const b = { sideLength: 100 };// Boom!// TypeError: attempted to get private field on non-instance// This fails because 'b' is not an instance of 'Square'.console.log(a.equals(b));
最後,對於任何普通 .js 檔案使用者,私有欄位始終必須在賦值之前宣告。
jsclass C {// No declaration for '#foo'// :(constructor(foo: number) {// SyntaxError!// '#foo' needs to be declared before writing to it.this.#foo = foo;}}
JavaScript 一直允許使用者訪問未宣告的屬性,而 TypeScript 一直要求宣告類屬性。有了私有欄位,無論是在 .js 檔案還是 .ts 檔案中,始終都需要進行宣告。
jsclass C {/** @type {number} */#foo;constructor(foo: number) {// This works.this.#foo = foo;}}
有關實現的更多資訊,你可以檢視原始 pull request。
我該使用哪一個?
我們已經收到了許多關於作為 TypeScript 使用者應該使用哪種私有屬性的問題:最常見的是,“我應該使用 private 關鍵字,還是 ECMAScript 的井號 (#) 私有欄位?”這取決於情況!
當涉及到屬性時,TypeScript 的 private 修飾符會被完全擦除——這意味著在執行時,它完全表現得像一個普通屬性,無法判斷它是用 private 修飾符宣告的。使用 private 關鍵字時,隱私僅在編譯時/設計時強制執行,對於 JavaScript 使用者來說,它完全是基於意圖的。
tsclass C {private foo = 10;}// This is an error at compile time,// but when TypeScript outputs .js files,// it'll run fine and print '10'.console.log(new C().foo); // prints '10'// ~~~// error! Property 'foo' is private and only accessible within class 'C'.// TypeScript allows this at compile-time// as a "work-around" to avoid the error.console.log(new C()["foo"]); // prints '10'
優點是這種“軟隱私”可以幫助你的使用者臨時規避無法訪問某些 API 的問題,並且可以在任何執行時中使用。
另一方面,ECMAScript 的 # 私有欄位在類外是完全無法訪問的。
tsclass C {#foo = 10;}console.log(new C().#foo); // SyntaxError// ~~~~// TypeScript reports an error *and*// this won't work at runtime!console.log(new C()["#foo"]); // prints undefined// ~~~~~~~~~~~~~~~// TypeScript reports an error under 'noImplicitAny',// and this prints 'undefined'.
這種硬隱私對於嚴格確保沒有人能夠利用你的內部實現非常有用。如果你是一名庫作者,刪除或重新命名私有欄位永遠不應導致破壞性更改。
如前所述,另一個好處是 ECMAScript 的 # 私有欄位可以使子類化更容易,因為它們確實是私有的。使用 ECMAScript # 私有欄位時,子類永遠不必擔心欄位命名的衝突。當涉及到 TypeScript 的 private 屬性宣告時,使用者仍然必須小心不要覆蓋超類中宣告的屬性。
另一個需要考慮的是你打算在哪裡執行程式碼。TypeScript 目前除非定位到 ECMAScript 2015 (ES6) 或更高版本,否則無法支援此功能。這是因為我們的降級實現使用 WeakMap 來強制實現隱私,而 WeakMap 無法以不導致記憶體洩漏的方式進行 polyfill。相比之下,TypeScript 的 private 宣告屬性適用於所有目標——甚至是 ECMAScript 3!
最後一個考慮因素可能是速度:private 屬性與其他任何屬性沒有區別,因此無論你定位到哪個執行時,訪問它們的速度都與其他屬性訪問一樣快。相反,因為 # 私有欄位是使用 WeakMap 降級的,所以使用它們可能會更慢。雖然某些執行時可能會最佳化其 # 私有欄位的實際實現,甚至擁有快速的 WeakMap 實現,但這可能並非在所有執行時中都是如此。
export * as ns 語法
擁有一個單一入口點,將另一個模組的所有成員作為單個成員公開,這很常見。
tsimport * as utilities from "./utilities.js";export { utilities };
這太常見了,以至於 ECMAScript 2020 最近添加了一種新語法來支援這種模式!
tsexport * as utilities from "./utilities.js";
這是對 JavaScript 的一項很好的體驗改進,TypeScript 3.8 實現了這種語法。當你的模組目標早於 es2020 時,TypeScript 將輸出類似於第一個程式碼片段的內容。
頂層 await
TypeScript 3.8 提供了對一種方便的即將推出的 ECMAScript 特性“頂層 await”的支援。
JavaScript 使用者經常為了使用 await 而引入一個 async 函式,並在定義後立即呼叫該函式。
jsasync function main() {const response = await fetch("...");const greeting = await response.text();console.log(greeting);}main().catch((e) => console.error(e));
這是因為在以前的 JavaScript(以及大多數具有類似功能的其他語言)中,await 僅允許在 async 函式體內使用。然而,透過頂層 await,我們可以在模組的頂層使用 await。
tsconst response = await fetch("...");const greeting = await response.text();console.log(greeting);// Make sure we're a moduleexport {};
注意有一個微妙之處:頂層 await 僅在模組的頂層工作,並且只有當 TypeScript 發現 import 或 export 時,檔案才被視為模組。在某些基礎情況下,你可能需要編寫 export {} 作為樣板程式碼來確保這一點。
頂層 await 在目前你可能期望的所有環境中可能無法正常工作。目前,你只能在 target 編譯器選項為 es2017 或更高版本,且 module 為 esnext 或 system 時使用頂層 await。在某些環境和打包工具中的支援可能有限,或者可能需要啟用實驗性支援。
有關我們實現的更多資訊,你可以檢視原始 pull request。
es2020 作為 target 和 module
TypeScript 3.8 支援將 es2020 作為 module 和 target 的選項。這將保留更新的 ECMAScript 2020 特性,如可選鏈 (optional chaining)、空值合併 (nullish coalescing)、export * as ns 和動態 import(...) 語法。這也意味著 bigint 字面量現在在 esnext 以下擁有了一個穩定的 target。
JSDoc 屬性修飾符
TypeScript 3.8 透過開啟 allowJs 標誌支援 JavaScript 檔案,並且還可以透過 checkJs 選項或在 .js 檔案頂部新增 // @ts-check 註釋來型別檢查這些 JavaScript 檔案。
由於 JavaScript 檔案沒有專用的型別檢查語法,TypeScript 利用了 JSDoc。TypeScript 3.8 理解了一些用於屬性的新 JSDoc 標籤。
首先是可訪問性修飾符:@public、@private 和 @protected。這些標籤的工作方式與 TypeScript 中的 public、private 和 protected 完全相同。
js// @ts-checkclass Foo {constructor() {/** @private */this.stuff = 100;}printStuff() {console.log(this.stuff);}}new Foo().stuff;// ~~~~~// error! Property 'stuff' is private and only accessible within class 'Foo'.
@public始終是隱式的,可以省略,表示屬性可以從任何地方訪問。@private表示屬性只能在包含類內使用。@protected表示屬性只能在包含類及其所有派生子類內使用,但不能在包含類的不同例項上使用。
接下來,我們還添加了 @readonly 修飾符,以確保屬性僅在初始化期間被寫入。
js// @ts-checkclass Foo {constructor() {/** @readonly */this.stuff = 100;}writeToStuff() {this.stuff = 200;// ~~~~~// Cannot assign to 'stuff' because it is a read-only property.}}new Foo().stuff++;// ~~~~~// Cannot assign to 'stuff' because it is a read-only property.
更好的 Linux 目錄監視和 watchOptions
TypeScript 3.8 推出了一種新的目錄監視策略,這對於高效獲取 node_modules 的變更至關重要。
背景是,在 Linux 等作業系統上,TypeScript 會在 node_modules 及其許多子目錄上安裝目錄監視器(而不是檔案監視器)來檢測依賴項的變化。這是因為可用檔案監視器的數量往往少於 node_modules 中的檔案數量,而需要跟蹤的目錄要少得多。
舊版本的 TypeScript 會立即在資料夾上安裝目錄監視器,啟動時這還可以;但在執行 npm install 時,node_modules 內會發生大量活動,這會使 TypeScript 不堪重負,經常導致編輯器會話速度變慢。為了防止這種情況,TypeScript 3.8 會在安裝目錄監視器之前稍作等待,讓這些高度波動的目錄有一些時間穩定下來。
由於每個專案在不同的策略下工作效果可能不同,並且這種新方法可能不適合你的工作流程,TypeScript 3.8 在 tsconfig.json 和 jsconfig.json 中引入了一個新的 watchOptions 欄位,允許使用者告知編譯器/語言服務應使用哪些監視策略來跟蹤檔案和目錄。
{// Some typical compiler options"": {"": "es2020","": "node"// ...},// NEW: Options for file/directory watching"watchOptions": {// Use native file system events for files and directories"": "useFsEvents","": "useFsEvents",// Poll files for updates more frequently// when they're updated a lot."": "dynamicPriority"}}
watchOptions 包含 4 個可以配置的新選項:
-
watchFile:監視單個檔案的策略。可以設定為:fixedPollingInterval:以固定的時間間隔每秒多次檢查每個檔案的更改。priorityPollingInterval:每秒多次檢查每個檔案的更改,但使用啟發式方法,比其他型別更少地檢查某些型別的檔案。dynamicPriorityPolling:使用動態佇列,其中較少修改的檔案檢查頻率較低。useFsEvents(預設值):嘗試使用作業系統/檔案系統的原生事件來監聽檔案更改。useFsEventsOnParentDirectory:嘗試使用作業系統/檔案系統的原生事件來監聽檔案所在目錄的更改。這可以使用更少的檔案監視器,但準確性可能較低。
-
watchDirectory:在缺乏遞迴檔案監視功能的系統上,監視整個目錄樹的策略。可以設定為:fixedPollingInterval:以固定的時間間隔每秒多次檢查每個目錄的更改。dynamicPriorityPolling:使用動態佇列,其中較少修改的目錄檢查頻率較低。useFsEvents(預設值):嘗試使用作業系統/檔案系統的原生事件來監聽目錄更改。
-
fallbackPolling:當使用檔案系統事件時,此選項指定當系統耗盡原生檔案監視器和/或不支援原生檔案監視器時使用的輪詢策略。可以設定為:fixedPollingInterval:(見上文。)priorityPollingInterval:(見上文。)dynamicPriorityPolling:(見上文。)synchronousWatchDirectory:停用目錄的延遲監視。延遲監視在可能同時發生大量檔案更改時很有用(例如執行npm install導致node_modules中的更改),但你可能希望在此處為某些不常見的設定停用它。
有關這些更改的更多資訊,請前往 GitHub 檢視 pull request 以閱讀更多內容。
“快速且寬鬆”的增量檢查
TypeScript 3.8 引入了一個名為 assumeChangesOnlyAffectDirectDependencies 的新編譯器選項。啟用此選項後,TypeScript 將避免重新檢查/重新構建所有真正可能受影響的檔案,而僅重新檢查/重新構建已更改的檔案以及直接匯入它們的檔案。
例如,考慮檔案 fileD.ts 匯入 fileC.ts,後者匯入 fileB.ts,後者匯入 fileA.ts,如下所示:
fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts
在 --watch 模式下,fileA.ts 的更改通常意味著 TypeScript 至少需要重新檢查 fileB.ts、fileC.ts 和 fileD.ts。在 assumeChangesOnlyAffectDirectDependencies 下,fileA.ts 的更改意味著只需要重新檢查 fileA.ts 和 fileB.ts。
在像 Visual Studio Code 這樣的程式碼庫中,這將某些檔案的更改重建時間從約 14 秒縮短到了約 1 秒。雖然我們不一定建議所有程式碼庫都使用此選項,但如果你擁有一個極其龐大的程式碼庫,並且願意將完整的專案錯誤檢查推遲到以後(例如透過 tsconfig.fullbuild.json 進行專門構建或在 CI 中進行),你可能會對此感興趣。
有關更多詳細資訊,你可以檢視原始 pull request。