TypeScript 3.8

僅型別匯入和匯出 (Type-Only Imports and Export)

對於大多數使用者來說,這個特性可能永遠用不上;但是,如果你在使用 isolatedModules、TypeScript 的 transpileModule API 或 Babel 時遇到過問題,那麼這個特性可能與你有關。

TypeScript 3.8 為僅型別匯入和匯出添加了新的語法。

ts
import type { SomeThing } from "./some-module.js";
export type { SomeThing };

import type 僅匯入用於型別註解和宣告的宣告。它始終會被完全擦除,因此在執行時不會留下任何痕跡。同樣,export type 也僅提供可用於型別上下文的匯出,並從 TypeScript 的輸出中擦除。

需要注意的是,類在執行時具有值,在設計時具有型別,且其用途是上下文敏感的。當使用 import type 匯入類時,你不能進行繼承等操作。

ts
import 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 類欄位提案的一部分。

ts
class 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 的可訪問性修飾符(如 publicprivate)不能用於私有欄位。
  • 私有欄位在包含類之外無法被訪問甚至無法被檢測到——即使是對 JS 使用者也是如此!有時我們稱之為硬隱私

除了“硬”隱私外,私有欄位的另一個好處是我們剛才提到的唯一性。例如,普通屬性宣告在子類中很容易被覆蓋。

ts
class 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'

使用私有欄位,你永遠不必擔心這一點,因為每個欄位名稱對於包含它的類來說都是唯一的。

ts
class 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

ts
class 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 檔案使用者,私有欄位始終必須在賦值之前宣告。

js
class 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 檔案中,始終都需要進行宣告。

js
class C {
/** @type {number} */
#foo;
constructor(foo: number) {
// This works.
this.#foo = foo;
}
}

有關實現的更多資訊,你可以檢視原始 pull request

我該使用哪一個?

我們已經收到了許多關於作為 TypeScript 使用者應該使用哪種私有屬性的問題:最常見的是,“我應該使用 private 關鍵字,還是 ECMAScript 的井號 (#) 私有欄位?”這取決於情況!

當涉及到屬性時,TypeScript 的 private 修飾符會被完全擦除——這意味著在執行時,它完全表現得像一個普通屬性,無法判斷它是用 private 修飾符宣告的。使用 private 關鍵字時,隱私僅在編譯時/設計時強制執行,對於 JavaScript 使用者來說,它完全是基於意圖的。

ts
class 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 的 # 私有欄位在類外是完全無法訪問的。

ts
class 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 語法

擁有一個單一入口點,將另一個模組的所有成員作為單個成員公開,這很常見。

ts
import * as utilities from "./utilities.js";
export { utilities };

這太常見了,以至於 ECMAScript 2020 最近添加了一種新語法來支援這種模式!

ts
export * as utilities from "./utilities.js";

這是對 JavaScript 的一項很好的體驗改進,TypeScript 3.8 實現了這種語法。當你的模組目標早於 es2020 時,TypeScript 將輸出類似於第一個程式碼片段的內容。

頂層 await

TypeScript 3.8 提供了對一種方便的即將推出的 ECMAScript 特性“頂層 await”的支援。

JavaScript 使用者經常為了使用 await 而引入一個 async 函式,並在定義後立即呼叫該函式。

js
async 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

ts
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);
// Make sure we're a module
export {};

注意有一個微妙之處:頂層 await 僅在模組的頂層工作,並且只有當 TypeScript 發現 importexport 時,檔案才被視為模組。在某些基礎情況下,你可能需要編寫 export {} 作為樣板程式碼來確保這一點。

頂層 await 在目前你可能期望的所有環境中可能無法正常工作。目前,你只能在 target 編譯器選項為 es2017 或更高版本,且 moduleesnextsystem 時使用頂層 await。在某些環境和打包工具中的支援可能有限,或者可能需要啟用實驗性支援。

有關我們實現的更多資訊,你可以檢視原始 pull request

es2020 作為 targetmodule

TypeScript 3.8 支援將 es2020 作為 moduletarget 的選項。這將保留更新的 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 中的 publicprivateprotected 完全相同。

js
// @ts-check
class 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-check
class 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.jsonjsconfig.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.tsfileC.tsfileD.ts。在 assumeChangesOnlyAffectDirectDependencies 下,fileA.ts 的更改意味著只需要重新檢查 fileA.tsfileB.ts

在像 Visual Studio Code 這樣的程式碼庫中,這將某些檔案的更改重建時間從約 14 秒縮短到了約 1 秒。雖然我們不一定建議所有程式碼庫都使用此選項,但如果你擁有一個極其龐大的程式碼庫,並且願意將完整的專案錯誤檢查推遲到以後(例如透過 tsconfig.fullbuild.json 進行專門構建或在 CI 中進行),你可能會對此感興趣。

有關更多詳細資訊,你可以檢視原始 pull request

TypeScript 文件是一個開源專案。透過傳送 Pull Request 來幫助我們改進這些頁面 ❤

此頁面的貢獻者
DRDaniel Rosenwasser (51)
OTOrta Therox (15)
ABAdam Boyce (1)
JBJack Bates (1)
MUMasato Urai (1)
2+

最後更新:2026 年 3 月 27 日