TypeScript 4.2

更智慧的類型別名保留

TypeScript 有一種為型別宣告新名稱的方法,稱為類型別名。如果你正在編寫一組都適用於 string | number | boolean 的函式,你可以編寫一個類型別名來避免反覆書寫相同的程式碼。

ts
type BasicPrimitive = number | string | boolean;

TypeScript 在列印輸出型別時,一直使用一套規則和推測來決定何時重用類型別名。例如,看看下面的程式碼片段。

ts
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
let x = value;
return x;
}

如果我們像在 Visual Studio、Visual Studio Code 或 TypeScript Playground 中那樣將滑鼠懸停在 x 上,會彈出一個顯示 BasicPrimitive 型別的簡要資訊面板。同樣,如果我們獲取該檔案的宣告檔案輸出(.d.ts 輸出),TypeScript 將宣告 doStuff 返回 BasicPrimitive

然而,如果我們返回 BasicPrimitiveundefined,會發生什麼呢?

ts
export type BasicPrimitive = number | string | boolean;
export function doStuff(value: BasicPrimitive) {
if (Math.random() < 0.5) {
return undefined;
}
return value;
}

我們可以檢視 TypeScript 4.1 Playground 中的結果。雖然我們可能希望 TypeScript 將 doStuff 的返回型別顯示為 BasicPrimitive | undefined,但它卻顯示為 string | number | boolean | undefined!這是怎麼回事?

這與 TypeScript 在內部表示型別的方式有關。當從一個或多個聯合型別建立聯合型別時,它總是會將這些型別標準化為一個新的扁平化聯合型別——但這樣做會丟失資訊。型別檢查器必須找到 string | number | boolean | undefined 中型別的每一種組合,以檢視可以使用哪些類型別名,即使如此,對於 string | number | boolean 也可能存在多個類型別名。

在 TypeScript 4.2 中,我們的內部實現變得更加智慧。我們透過保留原始程式碼的編寫和構建方式的相關部分,來跟蹤型別的構建過程。我們還會跟蹤並區分其他別名例項的類型別名!

能夠根據你在程式碼中的使用方式來回顯型別,意味著作為 TypeScript 使用者,你可以避免顯示一些極其冗長的型別,這通常可以轉化為更好的 .d.ts 檔案輸出、錯誤訊息,以及編輯器中簡要資訊和簽名幫助的型別展示。這可以使 TypeScript 對新手來說更加親切。

欲瞭解更多資訊,請檢視第一個改進聯合類型別名保留情況的拉取請求,以及第二個保留間接別名的拉取請求

元組型別中的前置/中間剩餘元素

在 TypeScript 中,元組型別旨在模擬具有特定長度和元素型別的陣列。

ts
// A tuple that stores a pair of numbers
let a: [number, number] = [1, 2];
// A tuple that stores a string, a number, and a boolean
let b: [string, number, boolean] = ["hello", 42, true];

隨著時間的推移,TypeScript 的元組型別變得越來越複雜,因為它們也被用來模擬 JavaScript 中的引數列表等內容。因此,它們可以擁有可選元素和剩餘元素,甚至可以為工具和可讀性新增標籤。

ts
// A tuple that has either one or two strings.
let c: [string, string?] = ["hello"];
c = ["hello", "world"];
 
// A labeled tuple that has either one or two strings.
let d: [first: string, second?: string] = ["hello"];
d = ["hello", "world"];
 
// A tuple with a *rest element* - holds at least 2 strings at the front,
// and any number of booleans at the back.
let e: [string, string, ...boolean[]];
 
e = ["hello", "world"];
e = ["hello", "world", false];
e = ["hello", "world", true, false, true];
Try

在 TypeScript 4.2 中,剩餘元素的使用方式得到了專門擴充套件。在以前的版本中,TypeScript 僅允許在元組型別的最後位置使用 ...rest 元素。

然而,現在剩餘元素可以出現在元組內的任何位置——僅有極少數限制。

ts
let foo: [...string[], number];
 
foo = [123];
foo = ["hello", 123];
foo = ["hello!", "hello!", "hello!", 123];
 
let bar: [boolean, ...string[], boolean];
 
bar = [true, false];
bar = [true, "some text", false];
bar = [true, "some", "separated", "text", false];
Try

唯一的限制是,只要剩餘元素後面沒有其他可選元素或剩餘元素,它就可以放置在元組中的任何位置。換句話說,每個元組只能有一個剩餘元素,且剩餘元素之後不能有可選元素。

ts
interface Clown {
/*...*/
}
interface Joker {
/*...*/
}
 
let StealersWheel: [...Clown[], "me", ...Joker[]];
A rest element cannot follow another rest element.1265A rest element cannot follow another rest element.
 
let StringsAndMaybeBoolean: [...string[], boolean?];
An optional element cannot follow a rest element.1266An optional element cannot follow a rest element.
Try

這些非尾部的剩餘元素可用於模擬那些接受任意數量前導引數,隨後跟有幾個固定引數的函式。

ts
declare function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): void;
 
doStuff(/*shouldCapitalize:*/ false)
doStuff("fee", "fi", "fo", "fum", /*shouldCapitalize:*/ true);
Try

儘管 JavaScript 沒有用於模擬前置剩餘引數的語法,但我們仍然能夠透過宣告 ...args 剩餘引數(其使用帶有前置剩餘元素的元組型別)來將 doStuff 宣告為接受前置引數的函式。這有助於對現有的許多 JavaScript 程式碼進行建模!

有關更多詳情,請參閱原始拉取請求

in 運算子進行更嚴格的檢查

在 JavaScript 中,在 in 運算子的右側使用非物件型別會導致執行時錯誤。TypeScript 4.2 確保可以在設計時捕獲此問題。

ts
"foo" in 42;
Type 'number' is not assignable to type 'object'.2322Type 'number' is not assignable to type 'object'.
Try

在大多數情況下,此檢查相當保守,因此如果你收到關於此的錯誤,很可能是程式碼本身存在問題。

非常感謝外部貢獻者 Jonas Hübotter 提交的拉取請求

--noPropertyAccessFromIndexSignature

TypeScript 最初引入索引簽名時,你只能透過“方括號”元素訪問語法(如 person["name"])來獲取它們宣告的屬性。

ts
interface SomeType {
/** This is an index signature. */
[propName: string]: any;
}
 
function doStuff(value: SomeType) {
let x = value["someProperty"];
}
Try

在處理具有任意屬性的物件時,這顯得非常繁瑣。例如,想象一個 API,其中由於在末尾多加了一個 s 字元而拼錯屬性名是很常見的。

ts
interface Options {
/** File patterns to be excluded. */
exclude?: string[];
 
/**
* It handles any extra properties that we haven't declared as type 'any'.
*/
[x: string]: any;
}
 
function processOptions(opts: Options) {
// Notice we're *intentionally* accessing `excludes`, not `exclude`
if (opts.excludes) {
console.error(
"The option `excludes` is not valid. Did you mean `exclude`?"
);
}
}
Try

為了簡化這類情況,前段時間,TypeScript 允許在型別具有字串索引簽名時使用“點號”屬性訪問語法(如 person.name)。這也使得將現有的 JavaScript 程式碼轉換為 TypeScript 變得更加容易。

然而,放寬此限制也意味著更容易拼錯顯式宣告的屬性。

ts
function processOptions(opts: Options) {
// ...
 
// Notice we're *accidentally* accessing `excludes` this time.
// Oops! Totally valid.
for (const excludePattern of opts.excludes) {
// ...
}
}
Try

在某些情況下,使用者更希望顯式地選擇使用索引簽名——他們希望在點號屬性訪問與特定屬性宣告不對應時收到錯誤訊息。

這就是為什麼 TypeScript 引入了一個名為 noPropertyAccessFromIndexSignature 的新標誌。在此模式下,你將選擇使用 TypeScript 舊有的報錯行為。此新設定不屬於 strict 標誌家族,因為我們認為使用者會發現它在某些程式碼庫中比在其他程式碼庫中更有用。

你可以透過閱讀相應的拉取請求來更詳細地瞭解此功能。我們還要特別感謝 Wenlu Wang 提交此拉取請求!

abstract 構造簽名

TypeScript 允許我們將類標記為 abstract(抽象類)。這告訴 TypeScript 該類僅用於被繼承,且某些成員需要由任何子類填充才能實際建立例項。

ts
abstract class Shape {
abstract getArea(): number;
}
 
new Shape();
Cannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.
 
class Square extends Shape {
#sideLength: number;
 
constructor(sideLength: number) {
super();
this.#sideLength = sideLength;
}
 
getArea() {
return this.#sideLength ** 2;
}
}
 
// Works fine.
new Square(42);
Try

為了確保在 new 運算子中使用 abstract 類的限制得到一致應用,你不能將 abstract 類賦值給任何期望構造簽名的內容。

ts
interface HasArea {
getArea(): number;
}
 
let Ctor: new () => HasArea = Shape;
Type 'typeof Shape' is not assignable to type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.2322Type 'typeof Shape' is not assignable to type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.
Try

這在如果我們打算執行 new Ctor 這樣的程式碼時是正確的,但在我們想要編寫 Ctor 的子類時,它就過於嚴格了。

ts
abstract class Shape {
abstract getArea(): number;
}
 
interface HasArea {
getArea(): number;
}
 
function makeSubclassWithArea(Ctor: new () => HasArea) {
return class extends Ctor {
getArea() {
return 42
}
};
}
 
let MyShape = makeSubclassWithArea(Shape);
Argument of type 'typeof Shape' is not assignable to parameter of type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Shape' is not assignable to parameter of type 'new () => HasArea'. Cannot assign an abstract constructor type to a non-abstract constructor type.
Try

它也無法與 InstanceType 等內建輔助型別很好地配合工作。

ts
type MyInstance = InstanceType<typeof Shape>;
Try

這就是為什麼 TypeScript 4.2 允許你在構造簽名上指定 abstract 修飾符。

ts
interface HasArea {
getArea(): number;
}
 
// Works!
let Ctor: abstract new () => HasArea = Shape;
Try

在構造簽名中新增 abstract 修飾符表示你可以傳入 abstract 建構函式。它並不會阻止你傳入其他“具體”的類/建構函式——它實際上只是表示沒有直接執行建構函式的意圖,因此傳入任一類型別都是安全的。

此功能允許我們以支援抽象類的方式編寫 mixin 工廠。例如,在下面的程式碼片段中,我們能夠將 mixin 函式 withStylesabstractSuperClass 一起使用。

ts
abstract class SuperClass {
abstract someMethod(): void;
badda() {}
}
 
type AbstractConstructor<T> = abstract new (...args: any[]) => T
 
function withStyles<T extends AbstractConstructor<object>>(Ctor: T) {
abstract class StyledClass extends Ctor {
getStyles() {
// ...
}
}
return StyledClass;
}
 
class SubClass extends withStyles(SuperClass) {
someMethod() {
this.someMethod()
}
}
Try

請注意,withStyles 演示了一條特定規則:一個擴充套件了通用且有抽象建構函式約束(如 Ctor)的值的類(如 StyledClass)也必須宣告為 abstract。這是因為無法知道是否傳入了具有更多抽象成員的類,因此無法確定子類是否實現了所有抽象成員。

你可以閱讀其拉取請求以瞭解更多關於抽象構造簽名的資訊。

使用 --explainFiles 理解專案結構

對於 TypeScript 使用者來說,一個非常常見的場景是問“為什麼 TypeScript 包含這個檔案?”。推斷程式的原始檔是一個複雜的過程,因此有很多原因會導致特定的 lib.d.ts 組合被使用,某些 node_modules 中的檔案被包含,以及某些檔案即使我們以為透過指定 exclude 會將它們排除在外,它們卻依然被包含進來。

這就是為什麼 TypeScript 現在提供了一個 explainFiles 標誌。

sh
tsc --explainFiles

使用此選項時,TypeScript 編譯器將輸出非常冗長的資訊,解釋某個檔案最終被包含在你的程式中的原因。為了更輕鬆地閱讀,你可以將輸出重定向到一個檔案,或者透過管道傳給一個易於檢視的程式。

sh
# Forward output to a text file
tsc --explainFiles > explanation.txt
# Pipe output to a utility program like `less`, or an editor like VS Code
tsc --explainFiles | less
tsc --explainFiles | code -

通常,輸出會首先列出包含 lib.d.ts 檔案的原因,然後是本地檔案,最後是 node_modules 檔案。

TS_Compiler_Directory/4.2.2/lib/lib.es5.d.ts
Library referenced via 'es5' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2015.d.ts
Library referenced via 'es2015' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2016.d.ts
Library referenced via 'es2016' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2017.d.ts
Library referenced via 'es2017' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2018.d.ts
Library referenced via 'es2018' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2019.d.ts
Library referenced via 'es2019' from file 'TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.es2020.d.ts
Library referenced via 'es2020' from file 'TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts'
TS_Compiler_Directory/4.2.2/lib/lib.esnext.d.ts
Library 'lib.esnext.d.ts' specified in compilerOptions
... More Library References...
foo.ts
Matched by include pattern '**/*' in 'tsconfig.json'

目前,我們不對輸出格式做出任何保證——它可能會隨時間而變化。關於這一點,如果你有任何建議,我們很樂意改進此格式!

欲瞭解更多資訊,請檢視原始拉取請求

改進邏輯表示式中對未呼叫函式的檢查

得益於 Alex Tarasyuk 的進一步改進,TypeScript 的未呼叫函式檢查現在應用於 &&|| 表示式中。

strictNullChecks 下,以下程式碼現在會報錯。

ts
function shouldDisplayElement(element: Element) {
// ...
return true;
}
function getVisibleItems(elements: Element[]) {
return elements.filter((e) => shouldDisplayElement && e.children.length);
// ~~~~~~~~~~~~~~~~~~~~
// This condition will always return true since the function is always defined.
// Did you mean to call it instead.
}

有關更多詳情,請檢視此處的拉取請求

解構變數可以顯式標記為未使用

得益於 Alex Tarasyuk 的另一個拉取請求,你現在可以透過在變數名前加上下劃線(_ 字元)來將解構變數標記為未使用。

ts
let [_first, second] = getValues();

以前,如果 _first 後面從未被使用,TypeScript 會在 noUnusedLocals 下報錯。現在,TypeScript 會識別出 _first 是有意以該下劃線命名,因為它本就沒有被使用的意圖。

有關更多詳情,請檢視完整更改

放寬可選屬性與字串索引簽名之間的規則

字串索引簽名是一種對字典類物件進行型別化的方法,你希望允許使用任意鍵進行訪問。

ts
const movieWatchCount: { [key: string]: number } = {};
 
function watchMovie(title: string) {
movieWatchCount[title] = (movieWatchCount[title] ?? 0) + 1;
}
Try

當然,對於任何尚未存在於字典中的電影標題,movieWatchCount[title] 將是 undefined(TypeScript 4.1 添加了選項 noUncheckedIndexedAccess,以在讀取此類索引簽名時包含 undefined)。儘管顯而易見會有某些字串不存在於 movieWatchCount 中,但由於 undefined 的存在,以前版本的 TypeScript 將可選物件屬性視為不可分配給否則相容的索引簽名。

ts
type WesAndersonWatchCount = {
"Fantastic Mr. Fox"?: number;
"The Royal Tenenbaums"?: number;
"Moonrise Kingdom"?: number;
"The Grand Budapest Hotel"?: number;
};
 
declare const wesAndersonWatchCount: WesAndersonWatchCount;
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
// ~~~~~~~~~~~~~~~ error!
// Type 'WesAndersonWatchCount' is not assignable to type '{ [key: string]: number; }'.
// Property '"Fantastic Mr. Fox"' is incompatible with index signature.
// Type 'number | undefined' is not assignable to type 'number'.
// Type 'undefined' is not assignable to type 'number'. (2322)
Try

TypeScript 4.2 允許了這種賦值。但是,它允許賦值包含 undefined 的非可選屬性,也不允許將 undefined 寫入特定鍵。

ts
type BatmanWatchCount = {
"Batman Begins": number | undefined;
"The Dark Knight": number | undefined;
"The Dark Knight Rises": number | undefined;
};
 
declare const batmanWatchCount: BatmanWatchCount;
 
// Still an error in TypeScript 4.2.
const movieWatchCount: { [key: string]: number } = batmanWatchCount;
Type 'BatmanWatchCount' is not assignable to type '{ [key: string]: number; }'. Property '"Batman Begins"' is incompatible with index signature. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.2322Type 'BatmanWatchCount' is not assignable to type '{ [key: string]: number; }'. Property '"Batman Begins"' is incompatible with index signature. Type 'number | undefined' is not assignable to type 'number'. Type 'undefined' is not assignable to type 'number'.
 
// Still an error in TypeScript 4.2.
// Index signatures don't implicitly allow explicit `undefined`.
movieWatchCount["It's the Great Pumpkin, Charlie Brown"] = undefined;
Type 'undefined' is not assignable to type 'number'.2322Type 'undefined' is not assignable to type 'number'.
Try

新規則也不適用於數字索引簽名,因為它們被假定為類似陣列且密集的。

ts
declare let sortOfArrayish: { [key: number]: string };
declare let numberKeys: { 42?: string };
 
sortOfArrayish = numberKeys;
Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'. Property '42' is incompatible with index signature. Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.2322Type '{ 42?: string | undefined; }' is not assignable to type '{ [key: number]: string; }'. Property '42' is incompatible with index signature. Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'.
Try

你可以透過閱讀原始 PR 來更好地理解此更改。

宣告缺失的輔助函式

得益於來自 Alexander Tarasyuk社群拉取請求,我們現在有了一個基於呼叫點宣告新函式和方法的快速修復!

An un-declared function foo being called, with a quick fix scaffolding out the new contents of the file

破壞性變更

我們始終努力最大限度地減少釋出中的破壞性更改。TypeScript 4.2 包含一些破壞性更改,但我們相信在升級過程中應該是可控的。

lib.d.ts 更新

與每個 TypeScript 版本一樣,lib.d.ts 的宣告(特別是為 Web 環境生成的宣告)發生了變化。雖然更改多種多樣,但 IntlResizeObserver 的宣告可能是最具破壞性的。

noImplicitAny 錯誤應用於鬆散的 yield 表示式

yield 表示式的值被捕獲,但 TypeScript 無法立即確定你打算讓它接收什麼型別(即 yield 表示式不是上下文型別的)時,TypeScript 現在會發出隱式 any 錯誤。

ts
function* g1() {
const value = yield 1;
'yield' expression implicitly results in an 'any' type because its containing generator lacks a return-type annotation.7057'yield' expression implicitly results in an 'any' type because its containing generator lacks a return-type annotation.
}
 
function* g2() {
// No error.
// The result of `yield 1` is unused.
yield 1;
}
 
function* g3() {
// No error.
// `yield 1` is contextually typed by 'string'.
const value: string = yield 1;
}
 
function* g4(): Generator<number, void, string> {
// No error.
// TypeScript can figure out the type of `yield 1`
// from the explicit return type of `g4`.
const value = yield 1;
}
Try

相應的更改中檢視更多詳情。

擴充套件的未呼叫函式檢查

如上所述,使用 strictNullChecks 時,未呼叫函式檢查現在將一致地執行在 &&|| 表示式中。這可能是一個新的破壞性來源,但通常是現有程式碼中邏輯錯誤的徵兆。

JavaScript 中的型別引數不再被解析為型別引數

型別引數在 JavaScript 中本就不被允許,但在 TypeScript 4.2 中,解析器將以更符合規範的方式解析它們。因此,當在 JavaScript 檔案中編寫以下程式碼時:

ts
f<T>(100);

TypeScript 將其解析為以下 JavaScript:

js
f < T > 100;

如果你一直在利用 TypeScript 的 API 在 JavaScript 檔案中解析型別結構(這在嘗試解析 Flow 檔案時可能發生),這可能會影響你。

參見拉取請求,獲取有關檢查內容的更多詳情。

擴充套件運算子的元組大小限制

元組型別可以透過在 TypeScript 中使用任何形式的展開語法(...)來建立。

ts
// Tuple types with spread elements
type NumStr = [number, string];
type NumStrNumStr = [...NumStr, ...NumStr];
// Array spread expressions
const numStr = [123, "hello"] as const;
const numStrNumStr = [...numStr, ...numStr] as const;

有時這些元組型別可能會意外變得巨大,這會導致型別檢查耗費很長時間。為了防止型別檢查過程掛起(這在編輯器場景中尤其糟糕),TypeScript 引入了一個限制器以避免進行所有這些工作。

你可以檢視此拉取請求以瞭解更多詳情。

.d.ts 副檔名不能用於匯入路徑

在 TypeScript 4.2 中,匯入路徑中包含 .d.ts 副檔名現在會報錯。

ts
// must be changed to something like
// - "./foo"
// - "./foo.js"
import { Foo } from "./foo.d.ts";

相反,你的匯入路徑應反映你的載入器在執行時將執行的操作。你可以改用以下任一匯入方式。

ts
import { Foo } from "./foo";
import { Foo } from "./foo.js";
import { Foo } from "./foo/index.js";

回退模板字面量推斷

此更改移除了 TypeScript 4.2 Beta 版本中的一個功能。如果你尚未升級到我們上一個穩定版本之後,你將不會受到影響,但你可能仍對這次更改感興趣。

TypeScript 4.2 的測試版包含對模板字串推斷的更改。在此更改中,模板字串字面量要麼被賦予模板字串型別,要麼簡化為多個字串字面量型別。這些型別在賦值給可變變數時會加寬string

ts
declare const yourName: string;
// 'bar' is constant.
// It has type '`hello ${string}`'.
const bar = `hello ${yourName}`;
// 'baz' is mutable.
// It has type 'string'.
let baz = `hello ${yourName}`;

這與字串字面量推斷的工作方式類似。

ts
// 'bar' has type '"hello"'.
const bar = "hello";
// 'baz' has type 'string'.
let baz = "hello";

因此,我們認為使模板字串表示式具有模板字串型別將是“一致的”;然而,從我們所見所聞來看,這並不總是可取的。

作為回應,我們撤銷了此功能(以及潛在的破壞性更改)。如果你確實希望模板字串表示式被賦予類似於字面量的型別,你總是可以在其末尾新增 as const

ts
declare const yourName: string;
// 'bar' has type '`hello ${string}`'.
const bar = `hello ${yourName}` as const;
// ^^^^^^^^
// 'baz' has type 'string'.
const baz = `hello ${yourName}`;

TypeScript 中 visitNodelift 回撥使用了不同的型別

TypeScript 有一個接受 lift 函式的 visitNode 函式。lift 現在期望一個 readonly Node[] 而不是 NodeArray<Node>。這在技術上是一個 API 破壞性更改,你可以在此處閱讀更多資訊。

TypeScript 文件是一個開源專案。請提交拉取請求來幫助我們改進這些頁面 ❤

此頁面的貢獻者
ELEliran Levi (1)
AGAnton Gilgur (1)
ABAndrew Branch (1)
JBJack Bates (1)
GGGabriel Goller (1)
3+

最後更新:2026 年 3 月 27 日