TypeScript 4.1

模板字面量型別

TypeScript 中的字串字面量型別允許我們對期望一組特定字串的函式和 API 進行建模。

ts
function setVerticalAlignment(location: "top" | "middle" | "bottom") {
// ...
}
 
setVerticalAlignment("middel");
Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.2345Argument of type '"middel"' is not assignable to parameter of type '"top" | "middle" | "bottom"'.
Try

這非常不錯,因為字串字面量型別基本上可以對我們的字串值進行拼寫檢查。

我們還喜歡將字串字面量用作對映型別中的屬性名。從這個意義上說,它們也可以用作構建塊。

ts
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

但字串字面量型別還可以作為另一個領域的構建塊:構建其他字串字面量型別。

這就是 TypeScript 4.1 引入模板字面量字串型別的原因。它具有與 JavaScript 中的模板字面量字串相同的語法,但用於型別位置。當你將其與具體的字面量型別一起使用時,它會透過拼接內容生成一個新的字串字面量型別。

ts
type World = "world";
 
type Greeting = `hello ${World}`;
type Greeting = "hello world"
Try

當替換位置有聯合型別時會發生什麼?它會產生由每個聯合成員所能表示的所有可能的字串字面量的集合。

ts
type Color = "red" | "blue";
type Quantity = "one" | "two";
 
type SeussFish = `${Quantity | Color} fish`;
type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"
Try

這不僅僅可以用於發行說明中的簡單示例。例如,一些 UI 元件庫可以透過其 API 指定垂直和水平對齊方式,通常使用像 "bottom-right" 這樣的單個字元串同時指定兩者。在垂直對齊 "top""middle""bottom" 以及水平對齊 "left""center""right" 之間,有 9 種可能的字串,其中每個前置字串都透過連字元與每個後置字串相連。

ts
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
 
// Takes
// | "top-left" | "top-center" | "top-right"
// | "middle-left" | "middle-center" | "middle-right"
// | "bottom-left" | "bottom-center" | "bottom-right"
 
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;
 
setAlignment("top-left"); // works!
setAlignment("top-middel"); // error!
Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.2345Argument of type '"top-middel"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
setAlignment("top-pot"); // error! but good doughnuts if you're ever in Seattle
Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.2345Argument of type '"top-pot"' is not assignable to parameter of type '"top-left" | "top-center" | "top-right" | "middle-left" | "middle-center" | "middle-right" | "bottom-left" | "bottom-center" | "bottom-right"'.
Try

雖然這類 API 在實際應用中很多,但這仍然只是一個玩具示例,因為我們可以手動寫出這些組合。事實上,對於 9 個字串來說這可能沒問題;但當你需要大量字串時,你應該考慮提前自動生成它們,以節省每次型別檢查的工作量(或者乾脆使用 string,這樣更容易理解)。

一些真正的價值來自於動態建立新的字串字面量。例如,想象一個 makeWatchedObject API,它接收一個物件並生成一個基本相同的物件,但帶有一個新的 on 方法來檢測屬性的變化。

ts
let person = makeWatchedObject({
firstName: "Homer",
age: 42, // give-or-take
location: "Springfield",
});
person.on("firstNameChanged", () => {
console.log(`firstName was changed!`);
});

請注意,on 監聽的是事件 "firstNameChanged",而不僅僅是 "firstName"。我們該如何為它編寫型別呢?

ts
type PropEventSource<T> = {
on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

有了這個,我們就可以構建出一種在我們給出錯誤屬性時報錯的東西!

ts
// error!
person.on("firstName", () => {});
Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.2345Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
 
// error!
person.on("frstNameChanged", () => {});
Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "ageChanged" | "locationChanged"'.
Try

我們還可以在模板字面量型別中做一些特別的事情:我們可以從替換位置推斷型別。我們可以使上一個示例通用化,從 eventName 字串的部分中推斷出關聯的屬性。

ts
type PropEventSource<T> = {
on<K extends string & keyof T>
(eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};
 
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
 
let person = makeWatchedObject({
firstName: "Homer",
age: 42,
location: "Springfield",
});
 
// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
// 'newName' has the type of 'firstName'
console.log(`new name is ${newName.toUpperCase()}`);
});
 
// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
if (newAge < 0) {
console.log("warning! negative age");
}
})
Try

在這裡,我們將 on 變成了一個泛型方法。當用戶使用字串 "firstNameChanged" 呼叫時,TypeScript 會嘗試為 K 推斷出正確的型別。為了做到這一點,它會將 K"Changed" 之前的內容進行匹配,並推斷出字串 "firstName"。一旦 TypeScript 確定了這一點,on 方法就可以獲取原始物件上 firstName 的型別,在本例中為 string。同樣,當我們用 "ageChanged" 呼叫時,它會找到屬性 age 的型別,即 number

推斷可以以不同的方式組合,通常用於解構字串,並以不同的方式重構它們。事實上,為了幫助修改這些字串字面量型別,我們添加了一些新的工具類型別名,用於修改字母的大小寫(即轉換為小寫和大寫字元)。

ts
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
 
type HELLO = EnthusiasticGreeting<"hello">;
type HELLO = "HELLO"
Try

新的類型別名是 UppercaseLowercaseCapitalizeUncapitalize。前兩個轉換字串中的每個字元,後兩個僅轉換字串中的第一個字元。

更多詳細資訊,請檢視原始拉取請求正在進行的切換到類型別名助手的拉取請求

對映型別中的鍵重對映

作為複習,對映型別可以基於任意鍵建立新的物件型別

ts
type Options = {
[K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean;
};
// same as
// type Options = {
// noImplicitAny?: boolean,
// strictNullChecks?: boolean,
// strictFunctionTypes?: boolean
// };

或者基於其他物件型別建立新的物件型別。

ts
/// 'Partial<T>' is the same as 'T', but with each property marked optional.
type Partial<T> = {
[K in keyof T]?: T[K];
};

到目前為止,對映型別只能產生具有你提供給它們的鍵的新物件型別;然而,很多時候你希望能夠基於輸入來建立新鍵或過濾掉某些鍵。

這就是為什麼 TypeScript 4.1 允許你透過新的 as 子句在對映型別中重新對映鍵。

ts
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// This is the new syntax!
}

有了這個新的 as 子句,你可以利用模板字面量型別等特性,輕鬆地基於舊屬性名建立新的屬性名。

ts
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
 
interface Person {
name: string;
age: number;
location: string;
}
 
type LazyPerson = Getters<Person>;
type LazyPerson = { getName: () => string; getAge: () => number; getLocation: () => string; }
Try

你甚至可以透過產生 never 來過濾掉鍵。這意味著在某些情況下,你不必使用額外的 Omit 輔助型別。

ts
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
 
interface Circle {
kind: "circle";
radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
type KindlessCircle = { radius: number; }
Try

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

遞迴條件型別

在 JavaScript 中,看到能夠以任意級別展平並構建容器型別的函式是很常見的。例如,考慮 Promise 例項上的 .then() 方法。.then(...) 會解開每個 Promise,直到找到一個“非 Promise”的值,並將該值傳遞給回撥。在 Array 上還有一個相對較新的 flat 方法,它可以接受一個深度引數來決定展平的深度。

在 TypeScript 的型別系統中表達這一點,在所有實際意義上都是不可能的。雖然有一些 hack 方法可以實現這一點,但最終的型別看起來非常不合理。

這就是 TypeScript 4.1 放寬了一些條件型別限制的原因——以便它們可以對這些模式進行建模。在 TypeScript 4.1 中,條件型別現在可以在其分支內立即引用自身,從而更容易編寫遞迴類型別名。

例如,如果我們想編寫一個獲取巢狀陣列元素型別的型別,我們可以編寫以下 deepFlatten 型別。

ts
type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;
function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
throw "not implemented";
}
// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

同樣,在 TypeScript 4.1 中,我們可以編寫一個 Awaited 型別來深度解開 Promise

ts
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
p: Promise<T>,
onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;

請記住,雖然這些遞迴型別很強大,但應負責任且節制地使用它們。

首先,這些型別可能會執行大量工作,這意味著它們會增加型別檢查時間。嘗試在 Collatz 猜想或斐波那契序列中模擬數字可能很有趣,但不要將這些內容釋出在 npm 的 .d.ts 檔案中。

除了計算密集之外,這些型別在處理足夠複雜的輸入時可能會達到內部遞迴深度限制。當達到該遞迴限制時,會導致編譯時錯誤。總的來說,最好完全不使用這些型別,而不是編寫在更現實的示例中失敗的程式碼。

檢視更多實現詳情

檢查索引訪問 (--noUncheckedIndexedAccess)

TypeScript 有一個稱為索引簽名的特性。這些簽名是一種向型別系統發出訊號的方式,即使用者可以訪問任意命名的屬性。

ts
interface Options {
path: string;
permissions: number;
 
// Extra properties are caught by this index signature.
[propName: string]: string | number;
}
 
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
 
// These are all allowed too!
// They have the type 'string | number'.
opts.yadda.toString();
opts["foo bar baz"].toString();
opts[Math.random()].toString();
}
Try

在上面的例子中,Options 有一個索引簽名,表示任何未列出的已訪問屬性都應具有 string | number 型別。這通常對於那些假設你知道自己在做什麼的樂觀程式碼來說很方便,但事實是 JavaScript 中的大多數值並不支援每個潛在的屬性名。例如,大多數型別不會擁有像前一個例子中由 Math.random() 建立的屬性鍵的值。對於許多使用者來說,這種行為是不可取的,並且感覺沒有充分利用 strictNullChecks 的嚴格檢查。

這就是為什麼 TypeScript 4.1 引入了一個名為 noUncheckedIndexedAccess 的新標誌。在這種新模式下,每個屬性訪問(如 foo.bar)或索引訪問(如 foo["bar"])都被認為是可能未定義的。這意味著在我們的最後一個例子中,opts.yadda 的型別將是 string | number | undefined,而不是僅僅是 string | number。如果你需要訪問該屬性,你要麼必須先檢查它的存在性,要麼使用非空斷言運算子(字尾 ! 字元)。

ts
function checkOptions(opts: Options) {
opts.path; // string
opts.permissions; // number
 
// These are not allowed with noUncheckedIndexedAccess
opts.yadda.toString();
'opts.yadda' is possibly 'undefined'.18048'opts.yadda' is possibly 'undefined'.
opts["foo bar baz"].toString();
Object is possibly 'undefined'.2532Object is possibly 'undefined'.
opts[Math.random()].toString();
Object is possibly 'undefined'.2532Object is possibly 'undefined'.
 
// Checking if it's really there first.
if (opts.yadda) {
console.log(opts.yadda.toString());
}
 
// Basically saying "trust me I know what I'm doing"
// with the '!' non-null assertion operator.
opts.yadda!.toString();
}
Try

使用 noUncheckedIndexedAccess 的一個後果是,即使在邊界檢查的迴圈中,對陣列的索引也會受到更嚴格的檢查。

ts
function screamLines(strs: string[]) {
// This will have issues
for (let i = 0; i < strs.length; i++) {
console.log(strs[i].toUpperCase());
Object is possibly 'undefined'.2532Object is possibly 'undefined'.
}
}
Try

如果你不需要索引,可以使用 for-of 迴圈或 forEach 呼叫來遍歷單個元素。

ts
function screamLines(strs: string[]) {
// This works fine
for (const str of strs) {
console.log(str.toUpperCase());
}
 
// This works fine
strs.forEach((str) => {
console.log(str.toUpperCase());
});
}
Try

此標誌對於捕獲越界錯誤很有用,但對於許多程式碼來說可能過於吵鬧,因此它不會被 strict 標誌自動啟用;但是,如果你對這個特性感興趣,請隨時嘗試,並確定它是否適合你團隊的程式碼庫!

你可以在實現此功能的拉取請求中瞭解更多資訊。

沒有 baseUrlpaths

使用路徑對映非常普遍——通常是為了有更好的匯入體驗,或者模擬 monorepo 的連結行為。

不幸的是,指定 paths 來啟用路徑對映還需要指定一個名為 baseUrl 的選項,這也會允許裸說明符路徑相對於 baseUrl 被解析。這通常還會導致自動匯入使用不佳的路徑。

在 TypeScript 4.1 中,paths 選項可以在沒有 baseUrl 的情況下使用。這有助於避免其中的一些問題。

checkJs 隱含 allowJs

以前,如果你要啟動一個經過檢查的 JavaScript 專案,你必須同時設定 allowJscheckJs。這稍微有些煩人,所以 checkJs 現在預設隱含了 allowJs

更多詳情請見拉取請求.

React 17 JSX 工廠

TypeScript 4.1 透過 jsx 編譯器選項的兩個新選項支援 React 17 即將推出的 jsxjsxs 工廠函式

  • react-jsx
  • react-jsxdev

這些選項分別用於生產和開發編譯。通常,其中一個選項可以繼承另一個。例如,用於生產構建的 tsconfig.json 可能如下所示

// ./src/tsconfig.json
{
"": "esnext",
"": "es2015",
"": "react-jsx",
"": true
},
"": ["./**/*"]
}

而用於開發構建的可能如下所示

// ./src/tsconfig.dev.json
{
"": "./tsconfig.json",
"": "react-jsxdev"
}
}

欲瞭解更多資訊,請檢視相應的 PR

JSDoc @see 標籤的編輯器支援

JSDoc @see 標籤現在在 TypeScript 和 JavaScript 編輯器中獲得了更好的支援。這允許你在標籤後的點號名稱上使用諸如“轉到定義”之類的功能。例如,在以下示例中,轉到 JSDoc 註釋中的 firstC 的定義均可正常工作

ts
// @filename: first.ts
export class C {}
// @filename: main.ts
import * as first from "./first";
/**
* @see first.C
*/
function related() {}

感謝頻繁貢獻者 Wenlu Wang 實現此功能

破壞性變更

lib.d.ts 變更

lib.d.ts 可能有一組 API 變更,這可能部分歸因於 DOM 型別自動生成的方式。一個具體的改變是 Reflect.enumerate 已被移除,因為它已在 ES2016 中被移除。

abstract 成員不能標記為 async

標記為 abstract 的成員不能再標記為 async。這裡的修復方法是移除 async 關鍵字,因為呼叫者只關心返回型別。

any/unknown 在假值位置被傳播

以前,對於像 foo && somethingElse 這樣的表示式,如果 foo 的型別是 anyunknown,則整個表示式的型別將是 somethingElse 的型別。

例如,之前 x 的型別是 { someProp: string }

ts
declare let foo: unknown;
declare let somethingElse: { someProp: string };
let x = foo && somethingElse;

然而,在 TypeScript 4.1 中,我們更加謹慎地確定這種型別。由於 && 左側的型別未知,我們將 anyunknown 向外傳播,而不是右側的型別。

我們看到的最常見的模式傾向於在檢查與 boolean 的相容性時發生,特別是在謂詞函式中。

ts
function isThing(x: any): boolean {
return x && typeof x === "object" && x.blah === "foo";
}

通常,合適的修復方法是將 foo && someExpression 切換為 !!foo && someExpression

resolve 的引數在 Promise 中不再是可選的

當編寫如下程式碼時

ts
new Promise((resolve) => {
doSomethingAsync(() => {
doSomething();
resolve();
});
});

你可能會遇到類似以下的錯誤

resolve()
~~~~~~~~~
error TS2554: Expected 1 arguments, but got 0.
An argument for 'value' was not provided.

這是因為 resolve 不再有可選引數,因此預設情況下,現在必須傳遞一個值。這通常可以捕獲使用 Promise 時的合法錯誤。典型的修復方法是傳遞正確的引數,有時還需要新增顯式的型別引數。

ts
new Promise<number>((resolve) => {
// ^^^^^^^^
doSomethingAsync((value) => {
doSomething();
resolve(value);
// ^^^^^
});
});

然而,有時 resolve() 確實需要不帶引數地呼叫。在這些情況下,我們可以給 Promise 一個顯式的 void 泛型型別引數(即寫成 Promise<void>)。這利用了 TypeScript 4.1 中的新功能,即潛在的 void 尾隨引數可以變為可選。

ts
new Promise<void>((resolve) => {
// ^^^^^^
doSomethingAsync(() => {
doSomething();
resolve();
});
});

TypeScript 4.1 附帶了一個快速修復程式來幫助解決這個破壞性更改。

條件傳播建立可選屬性

在 JavaScript 中,物件傳播(如 { ...foo })不對假值進行操作。因此,在像 { ...foo } 這樣的程式碼中,如果 foonullundefined,它將被跳過。

許多使用者利用這一點來“有條件地”傳播屬性。

ts
interface Person {
name: string;
age: number;
location: string;
}
interface Animal {
name: string;
owner: Person;
}
function copyOwner(pet?: Animal) {
return {
...(pet && pet.owner),
otherStuff: 123,
};
}
// We could also use optional chaining here:
function copyOwner(pet?: Animal) {
return {
...pet?.owner,
otherStuff: 123,
};
}

在這裡,如果 pet 被定義,pet.owner 的屬性將被傳播進來——否則,返回的物件中將不會傳播任何屬性。

copyOwner 的返回型別以前是基於每次傳播的聯合型別

{ x: number } | { x: number, name: string, age: number, location: string }

這準確地模擬了操作發生的方式:如果 pet 被定義,那麼 Person 中的所有屬性都將存在;否則,它們都不會在結果中定義。這是一個全有或全無的操作。

然而,我們已經看到這種模式被推向了極端,單個物件中有數百次傳播,每次傳播都可能增加數百或數千個屬性。事實證明,由於各種原因,這最終變得極其昂貴,而且通常沒有什麼好處。

在 TypeScript 4.1 中,返回的型別有時會使用全可選屬性。

{
x: number;
name?: string;
age?: number;
location?: string;
}

這最終表現更好,而且顯示效果也更好。

更多詳情,請參閱原始更改。雖然此行為目前並不完全一致,但我們預計未來的版本將產生更清晰和更可預測的結果。

TypeScript 以前會將不對應的引數透過關聯到 any 型別來聯絡起來。隨著 TypeScript 4.1 中的更改,語言現在完全跳過了這個過程。這意味著某些可賦值性的情況現在會失敗,但也意味著某些過載解析的情況也會失敗。例如,Node.js 中 util.promisify 的過載解析在 TypeScript 4.1 中可能會選擇不同的過載,有時會導致下游出現新的或不同的錯誤。

作為變通方法,你最好使用型別斷言來壓制錯誤。

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

此頁面的貢獻者
EIEugene Ilyin (1)
ABAndrew Branch (1)
JBJack Bates (1)
HHumanEquivalentUnit (1)
OTOrta Therox (1)

最後更新:2026 年 3 月 27 日