TypeScript 4.4 RC版來了,正式版將于月底發(fā)布
今天,我們很高興地宣布 TypeScript 4.4 候選版本(RC)已經(jīng)到來!因此從現(xiàn)在起到 TypeScript 4.4 穩(wěn)定版,除了對關鍵 bug 做出修復之外,預計不會再有其他更深層次的變化調整。如果你想現(xiàn)在就嘗試 TypeScript 的 RC 版,可以通過 NuGet 獲取,或者使用以下 npm 命令:
npm install typescript@rc
TypeScript 4.4 版本中的部分主要亮點包括別名條件與判別式的控制流分析、符號與模板字符串模式索引簽名、性能改進、JavaScript 拼寫建議等。
下面就來一起看看吧!
在 JavaScript 當中,我們往往需要以不同的方式探測同一變量,查看它是否有我們可以使用的具體類型。TypeScript 能夠理解這些探測操作,并將其設定為類型守衛(wèi)(type guard)。類型檢查器會使用“控制流分析”機制推斷每個語言構造中的類型,這就省去了在使用時對 TypeScript 變量類型做出聲明的麻煩。
例如,我們可以寫出如下代碼形式:
function foo(arg: unknown) {
if (typeof arg === "string") {
// 現(xiàn)在我們知道這是一條字符串。
console.log(arg.toUpperCase());
}
}
在此示例中,我們會檢查 arg 是否是一條 string。TypeScript 識別出了 typeof arg === "string" 檢查,將其理解為類型守衛(wèi),并能夠判斷出 arg 應該是 if 塊主體中的 string。
但是,如果我們把條件變更為常量,結果又將如何?
function foo(arg: unknown) {
const argIsString = typeof arg === "string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// 錯誤!類型「unknown」上不存在屬性「toUpperCase」。
}
}
在以往的 TypeScript 版本中,這會觸發(fā)一項錯誤——即使 argIsString 被分配到了類型守衛(wèi)值,TypeScript 也只會丟失該信息。這不科學,畢竟用戶很可能希望在多個位置重復執(zhí)行相同的檢查。為了解決這個問題,之前大家只能重復操作或者使用類型斷言(強制轉換)。
但在 TypeScript 4.4 中,問題已不復存在。以上示例不會引發(fā)任何錯誤!當 TypeScript 發(fā)現(xiàn)我們在測試某個常量值時,它會執(zhí)行一些額外的操作以查看其中是否包含類型守衛(wèi)。如果該類型守衛(wèi)對 const、readonly 屬性或者未修改的參數(shù)執(zhí)行操作,則 TypeScript 能夠適當縮小該值。
除 typeof 檢查之外,TypeScript 還提供多種不同的類型守衛(wèi)條件。例如,對 charm 等可區(qū)分聯(lián)合進行檢查。
type Shape =
| { kind: "circle", radius: number }
| { kind: "square", sideLength: number };
function area(shape: Shape): number {
const isCircle = shape.kind === "circle";
if (isCircle) {
// 我們知道這里有個圓!
return Math.PI * shape.radius ** 2;
}
else {
// 我們知道這里有個正方形!
return shape.sideLength ** 2;
}
}
4.4 版本對于判別式的分析也更為深入——現(xiàn)在,大家可以提取出判別式,而 TypeScript 則能夠縮小原始對象的范圍。
type Shape =
| { kind: "circle", radius: number }
| { kind: "square", sideLength: number };
function area(shape: Shape): number {
// 首先提取出「kind」字段。
const { kind } = shape;
if (kind === "circle") {
// 我們知道這里有個圓!
return Math.PI * shape.radius ** 2;
}
else {
// 我們知道這里有個正方形!
return shape.sideLength ** 2;
}
}
再舉一例,以下函數(shù)用于檢查兩個輸入中是否有內容。
function doSomeChecks(
inputA: string | undefined,
inputB: string | undefined,
shouldDoExtraWork: boolean,
) {
let mustDoWork = inputA && inputB && shouldDoExtraWork;
if (mustDoWork) {
// 能夠訪問'inputA'與'inputB'上的「string」屬性!
const upperA = inputA.toUpperCase();
const upperB = inputB.toUpperCase();
// ...
}
}
TypeScript 能夠理解在 mustDoWork 為 true 的情況下,inputA 與 inputB 都存在。這意味著我們用不著再編寫像 inputA! 這樣的非空斷言來向 TypeScript 強調 inputA 并非 undefined。
更奇妙的是,這種分析機制是可以傳遞的。如果我們將某個常量分配給某個包含多個常量的條件,而且各個常量都被分配到了類型守衛(wèi),那么 TypeScript 隨后即可傳遞這些條件。
function f(x: string | number | boolean) {
const isString = typeof x === "string";
const isNumber = typeof x === "number";
const isStringOrNumber = isString || isNumber;
if (isStringOrNumber) {
x; // 'x'的類型為'string | number'。
}
else {
x; // 'x'的類型為'boolean'。
}
}
請注意,新機制的深度是有極限的——TypeScript 在檢查這些條件時不會過度深入,但對大多數(shù)日常檢查來說應該是足夠了。
這項功能應該會讓更多 JavaScript 代碼能夠直接在 TypeScript 中“正常起效”。關于更多詳細信息,請參閱 GitHub 上的實現(xiàn)。
鏈接:https://github.com/microsoft/TypeScript/pull/44730
TypeScript 允許大家使用索引簽名來描述各個屬性都必須具備的特定對象。如此一來,我們就能將這些對象作為類似于字典的類型,并在其中通過中括號使用字符串鍵對它們進行索引。
例如,我們可以編寫一個帶有索引簽名的類型,此類型接收 string 鍵并映射為相應的 boolean 值。如果我們嘗試分配 boolean 值以外的值,則返回錯誤。
interface BooleanDictionary {
[key: string]: boolean;
}
declare let myDict: BooleanDictionary;
// 分配 boolean 值有效
myDict["foo"] = true;
myDict["bar"] = false;
// 錯誤,"oops"不是 boolean 值
myDict["baz"] = "oops";
雖然這里使用 Map 數(shù)據(jù)結構可能更好(即 Map<string, boolean>),但這里考慮的是 JavaScript 對象的易用性更強、或者是項目恰好這么要求。
同樣的,Array
// 這里是 TypeScript 內置 Array 類型定義的一部分。
interface Array<T> {
[index: number]: T;
// ...
}
let arr = new Array<string>();
// 有效
arr[0] = "hello!";
// 錯誤,這里需要一個「string」值
arr[1] = 123;
索引簽名特別適用于在外部表達大量代碼的情況;但到目前為止,索引簽名僅適用于 string 及 number 鍵(而且 string 索引中還故意設置一項特性,即可以接受 number 鍵,這是因為數(shù)字鍵總會被強制轉換為字符串)。換句話說,TypeScript 不允許使用 symbol 鍵作為索引對象。TypeScript 也無法對某些 string 鍵子集的索引簽名進行建模——例如用于描述一切以文本 data- 作為名稱開頭的屬性的索引簽名。
TypeScript 4.4 解決了上述限制,已經(jīng)將索引簽名的適用范圍拓展到符號與模板字符串模式當中。
例如,TypeScript 現(xiàn)在允許用戶聲明采用任意 symbol 鍵的類型。
interface Colors {
[sym: symbol]: number;
}
const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");
let colors: Colors = {};
colors[red] = 255; // 允許賦值
let redVal = colors[red]; // 'redVal'的類型為'number'
colors[blue] = "da ba dee"; // 錯誤:類型'string'無法分配給類型'number'。
同樣的,我們也可以使用模板客串模式類型編寫索引簽名。這種作法常見于篩選操作,例如在 TypeScript 的多余屬性檢查中剔除一切以 data- 開頭的屬性。當我們將對象字面量傳遞給具有預期類型的內容時,TypeScript 即可檢查未在預期類型中得到聲明的多余屬性。
interface Options {
width?: number;
height?: number;
}
let a: Options = {
width: 100,
height: 100,
"data-blah": true, // 錯誤!「data-blah」未在「Options」中聲明。
};
interface OptionsWithDataProps extends Options {
// 允許任何以'data-'開頭的屬性。
[optName: `data-${string}`]: unknown;
}
let b: OptionsWithDataProps = {
width: 100,
height: 100,
"data-blah": true, // 成功了!
"unknown-property": true, // 錯誤!'unknown-property' 未在'OptionsWithDataProps'中聲明。
};
關于索引簽名的最后一項要點是,其現(xiàn)在可以支持無限域原始類型的聯(lián)合,具體包括:
string
number
symbol
模板字符串模式 (例如
hello-${string})
參數(shù)為這些類型的聯(lián)合的索引簽名將脫糖為幾個不同的索引簽名。
interface Data {
[optName: string | symbol]: any;
}
// 等價于
interface Data {
[optName: string]: any;
[optName: symbol]: any;
}
在 JavaScript 中,任何值的類型都可使用 throw 拋出并在 catch 子句中進行捕捉。因此,TypeScript 以往一直將 catch 子句變量類型化為 any,且不允許任何其他類型注釋:
try {
// 誰知道這會拋出什么...
executeSomeThirdPartyCode();
}
catch (err) { // err: any
console.error(err.message); // 允許,因為符合'any'
err.thisWillProbablyFail(); // 允許,因為符合'any' :(
}
這一次,TypeScript 迎來了 unknown 類型;對于需要盡可能提高正確性與類型安全性的用戶來說,unknown 在 catch 子句中顯然要比 any 更好,因為它可以更好地縮小范圍并迫使我們針對任意值做出測試。最終,TypeScript 4.0 版本開始允許用戶在各個 catch 子句變量上指定 unknown (或者 any) 的顯式類型注釋,以便根據(jù)具體情況選擇更嚴格的類型;但對很多開發(fā)者來說,在每一個 catch 子句上手動指定: unknown 確實非常麻煩。
因此,TypeScript 4.4 引入了一個名為 --useUnknownInCatchVariables 的新標記。此標記能夠將 catch 子句變量的默認類型由 any 變更為 unknown。
try {
executeSomeThirdPartyCode();
}
catch (err) { // err: unknown
// 錯誤!類型'unknown'上不存在'message'。
console.error(err.message);
// 成功了!我們可以將'err'由'unknown'縮小為'Error'。
if (err instanceof Error) {
console.error(err.message);
}
}
此標記歸屬于 --strict 選項系列。所以如果您使用 --strict 檢查代碼,此選項將自動開啟。但您也可能在 TypeScript 4.4 上遇到如下錯誤:
類型'unknown'上不存在屬性'message'。
類型'unknown'上不存在屬性'name'。
類型'unknown'上不存在屬性'stack'。
如果我們不想在 catch 子句中處理 unknown 變量,則可以始終添加明確的 : any 注釋以聲明不使用更嚴格的類型。
try {
executeSomeThirdPartyCode();
}
catch (err: any) {
console.error(err.message); // 再次成功!
}
在 JavaScript 當中,讀取對象上的屬性缺失會產(chǎn)生 undefined 值。當然,也可能有某些實際屬性的值確實為 undefined。JavaScript 中的很多代碼都傾向于相同的方式處理這些情況,所以以其為基礎的 TypeScript 最初也只是解釋每個可選屬性,類似于用戶在類型中寫入了 undefined。例如:
interface Person {
name: string,
age?: number;
}
被認為等價于
interface Person {
name: string,
age?: number | undefined;
}
這意味著用戶可以明確使用 undefined 代替 age。
const p: Person = {
name: "Daniel",
age: undefined, // 默認情況下沒有問題。
};
因此,TypeScript 在默認情況下并不能區(qū)分實際值為 undefined 的屬性與缺失的屬性。雖然大多數(shù)情況下這并不是什么問題,但也有一些 JavaScript 代碼會做出不同的假設。Object.assign, Object.keys, object spread ({ ...obj }) 以及 for–in 循環(huán)等函數(shù)及運算符的行為都取決于對象之上是否實際存在屬性。在我們的 Person 示例中,如果 age 屬性出現(xiàn)在很重要的上下文信息當中,則很可能引導運行時錯誤。
在 TypeScript 4.4 中,新的標記 –exactOptionalPropertyTypes 負責強調完全按字面形式解釋各個可選屬性類型,也就是說 | undefined 不會被添加至類型當中:
// 當啟用'exactOptionalPropertyTypes'時:
const p: Person = {
name: "Daniel",
age: undefined, // 錯誤!undefined 不是數(shù)字
};
此標記并不屬于 --strict 系列,所以如果需要這種功能,請明確將其啟用。另外,它還要求啟用 --strictNullChecks。我們將陸續(xù)更新 DefinitelyTyped 與其他更多定義,盡可能幫助大家降低轉換難度;當然,根據(jù)實際代碼結構的不同,您也可能會遇到某些具體問題。
TypeScript 4.4 還支持在類中使用 static 塊。這是一項即將推出的 ECMAScript 功能,可幫助您為靜態(tài)成員編寫出更復雜的初始化代碼。
class Foo {
static count = 0;
// 此為 static 塊:
static {
if (someCondition()) {
count++;
}
}
}
這些 static 塊允許您編寫具有自身范圍的語句序列,由這些語句訪問包含類之內的私有字段。換句話說,我們能夠編寫出具備所編寫語句全部功能的初始化代碼,可以在完全訪問類內容的同時不致泄露變量。
class Foo {
static #count = 0;
get count() {
return this.#count;
}
static {
try {
const lastInstances = loadLastInstances();
count += lastInstances.length;
}
catch {}
}
}
如果沒有 static 塊,我們也可以使用上述代碼,但會在不同的類型里留下安全隱患。
請注意,同一個類可以包含多個 static 塊,各個塊的運行順序等同于其編寫順序。
// Prints:
// 1
// 2
// 3
class Foo {
static prop = 1
static {
console.log(1);
}
static {
console.log(2);
}
static {
console.log(3);
}
}
TypeScript 的 --help 選項已經(jīng)迎來更新!感謝 Song Gao 的辛勤工作,我們成功調整并更新了編譯器選項的描述,并使用顏色及其他視覺元素重新設計了 --help 菜單的樣式。目前我們仍在對設計樣式進行迭代,希望默認主題能在各個平臺上正常工作,大家也可以參考原始提案了解新菜單的基本外觀。
https://github.com/microsoft/TypeScript/issues/44074
聲明發(fā)布速度更快
TypeScript 正在考量內部符號能否在不同上下文中訪問,以及應如何打印特定類型。這些變量有望提高 TypeScript 在高復雜度代碼中的整體性能,特別是在使用 --declaration 標記的.d.ts 文件發(fā)布場景之下。
路徑歸一化速度更快
TypeScript 往往需要對各種文件路徑類型進行“歸一化”,確保將其轉換為編譯器能夠隨處使用的統(tǒng)一格式。具體操作包括使用斜杠來替換反斜杠,或者刪除路徑中的 /./ 以及 /../ 等等。但在處理包含數(shù)百萬條路徑的龐大項目時,這類操作終究會拖慢工作進度。所以 TypeScript 4.4 會首先對路徑進行快速檢查,查看其是否需要進行歸一化處理。這項改進將大型項目的加載時長縮短了 5% 到 10%;我們在內部對大型項目進行測試時,發(fā)現(xiàn)加載時間確實明顯改善。
路徑映射速度更快
TypeScript 希望加快構建路徑映射的速度(使用 tsconfig.json 中的 paths 選項)。對于包含數(shù)百個映射的項目,由此帶來的性能提升相當顯著。
使用 –strict 加快增量構建
我們發(fā)現(xiàn)了一個 bug,即如果 --strict 處于啟用狀態(tài),那么 TypeScript 最終會在 --incremental 編譯下重新執(zhí)行類型檢查。這會導致不少構建操作如同 --incremental 被關閉了一樣緩慢。TypeScript 4.4 修復了這個問題,同時也將修復成果向下移植到了 TypeScript 4.3 當中。
為大型輸出更快生成源映射
TypeScript 4.4 為超大輸出文件提供了源映射生成優(yōu)化功能。與舊版 TypeScript 編譯器相比,新版本的發(fā)布時長可縮短約 8%。
--force 構建速度更快
在項目引用中使用 --build 模式時,TypeScript 必須執(zhí)行最新檢查以確定需要重建哪些文件。但在執(zhí)行 --force 構建時,TypeScript 卻不會使用這部分信息,而是對所有項目依賴項均從零開始構建。在 TypeScript 4.4 中,--force 構建也能根據(jù)檢查結果確定需要重建的具體文件了。
TypeScript 為 Visual Studio 及 Visual Studio Code 等編輯器中的 JavaScript 編輯體驗提供支持。大多數(shù)情況下,TypeScript 會盡量不干涉 JavaScript 文件,但也會根據(jù)實際情況提出一些置信度高、且不太具有破壞性影響的建議方法。
因此,現(xiàn)在即使是沒有開啟 // @ts-check 或者 checkJs 的項目,TypeScript 也會為純 JavaScript 文件提供拼寫建議。這些建議與 TypeScript 文件中的“Did you mean…?”形式完全相同。
拼寫建議中的線索能夠幫助您查找代碼中的錯誤。我們也在測試中成功從現(xiàn)有代碼中找出了不少錯誤!
關于此項功能的更多詳細信息,請 參閱 pull 請求。
TypeScript 4.4 提供對 inlay hints 的支持,可幫助您在代碼中顯示有用信息,包括參數(shù)名稱與返回類型。這相當于一種友好的“幽靈文本”。

這項功能由 Wenlu Wang 貢獻,點擊下方鏈接可查看 pull 請求中的詳細信息。
https://github.com/microsoft/TypeScript/pull/42089
Wenlu 還貢獻了 Visual Studio Code 中的 inlay hints 集成,并隨 2021 年 7 月的 1.59 版本共同發(fā)布。如果您想體驗 inlay hints,請確保您使用的是最新的穩(wěn)定版或內部版編輯器。您也可以在修改設置中調整 inlay hints 提示的時間與位置。
在 Visual Studio Code 等編輯器顯示完成列表時,具有自動導入的完成結果會在顯示中包含對于特定模塊的路徑。然而,此路徑往往并不是由 TypeScript 親自放置在模塊說明當中。此路徑通常與工作區(qū)相關,所以如果您是從 moment 等工具包處進行導入,則會看到 node_modules/moment 之類的路徑。

因為沒有正確考慮到 Node 的 node_modules 解析、路徑映射、符號鏈接與重新導出等因素,這些路徑往往會產(chǎn)生一定的誤導效果。

由于這項功能會帶來較高的計算資源需求,因此在鍵入大量字符時,包含眾多自動導入的完成項列表可能會批量填充最終模塊說明。所以有時候您看到的可能仍是舊的工作區(qū)相關路徑標簽;但隨著編輯器的不斷“預熱”,您應該很快就會看到正確的導入路徑。
TypeScript 4.4 中的 lib.d.ts 變更
與之前的各個版本一樣,TypeScript 4.4 中的 lib.d.ts 聲明(特別是為 Web 上下文生成的聲明)再次變更。您可以參閱我們的 lib.dom.d.ts 變更列表以了解新增內容。
間接調用導入函數(shù)以提升合規(guī)性
在其他早期版本中,從 CommonJS、AMD 以及其他非 ES 模塊系統(tǒng)處執(zhí)行的導入調用操作會設置所調用函數(shù)的 this 值。具體來講,在以下示例中,當我們調用 fooModule.foo() 時, foo() 方法會將 fooModule 設置為 this 的值。
// 假設這是我們導入的模塊,它有一個名為'foo'的導出。
let fooModule = {
foo() {
console.log(this);
}
};
fooModule.foo();
但 ECMAScript 在處理導出函數(shù)時的方式與此不同。所以,我們才決定在 TypeScript 4.4 的導入函數(shù)調用中丟棄掉 this 值。
// 假設這是我們導入的模塊,它有一個名為'foo'的導出。
let fooModule = {
foo() {
console.log(this);
}
};
// 請注意,現(xiàn)在我們實際調用的是'(0, fooModule.foo)' 。
(0, fooModule.foo)();
在 Catch 變量中使用 unknown
用戶在運行 --strict 標記時可能看到關于 catch 變量為 unknown 的新錯誤,特別是在現(xiàn)有代碼假定只捕捉了 Error 值的時候。這通常會引發(fā)發(fā)下錯誤提示:
類型'unknown'上不存在屬性'message'。
類型'unknown'上不存在屬性'name'。
類型'unknown'上不存在屬性'stack'。
要解決這個問題,您可以添加專門的運行時檢查以保證拋出的類型與您的預期類型相符。此外,您也可以使用類型斷言,向您的 catch 變量添加顯式的: any,或者干脆關閉 --useUnknownInCatchVariables。
更廣泛的始終為真承諾檢查
在之前的版本中,TypeScript 引用了“始終為真承諾檢查”(Always Truthy Promise checks)來捕捉可能遺留有 await 的代碼。但這項檢查僅適用于命名聲明,所以雖然代碼能夠正確接收到錯誤,但:
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
const fooResult = foo();
if (fooResult) { // <- 出錯了! :D
return "true";
}
return "false";
}
……以下代碼卻得不到提示。
async function foo(): Promise<boolean> {
return false;
}
async function bar(): Promise<string> {
if (foo()) { // <- 沒有錯誤提示 :(
return "true";
}
return "false";
}
TypeScript 4.4 現(xiàn)在能夠對二者做出正確標記。
抽象屬性不能有初始化器
以下代碼現(xiàn)在會引發(fā)錯誤,這是因為抽象屬性不能有初始化器:
abstract class C {
abstract prop = 1;
// ~~~~
// 因為被標記為抽象,所以屬性'prop' 不能有初始化器。
}
相反,您只能為屬性指定類型:
abstract class C {
abstract prop: number;
}
我們計劃在未來幾周之內發(fā)布 TypeScript 4.4 的穩(wěn)定版,感興趣的朋友請持續(xù)關注我們的 4.4 迭代計劃。具體鏈接:
https://github.com/microsoft/TypeScript/issues/44237
原文鏈接:https://devblogs.microsoft.com/typescript/announcing-typescript-4-4-rc/
