大家好,我是 Echa。
11 月 1 日,TypeScript 4.9 發布了候選版本 (RC),直到穩定版發布基本上不會有太大變化了,本次帶來的更新還是挺有意思的,下面我就跟大家來一起看一下~
新的 satisfies 操作符
在使用 TypeScript 類型推斷的時候,有很多情況下會讓我們面臨兩難的選擇:我們即希望確保某些表達式能夠匹配某些類型,但也希望保留這個表達式的特定類型用來類型推斷。
比如下面的例子,我們定義了一個顏色選擇對象:
const palette = { red: [255, 0, 0], green: "#00ff00", blue: [0, 0, 255] };
因為每個屬性都被賦予了默認值,ts 會自動幫我們自動推導 palette 的屬性類型,所以我們可以直接調用它們的方法:
// red 被推斷為 Number[] 類型 const a = palette.red.at(0); // green 被推斷為 string 類型 const b = palette.green.toUpperCase();
因為顏色都是固定的,我們想讓我們的 palette 對象擁有特定的幾個屬性,來避免我們寫出一些錯別字:
const palette = { // 錯別字:rad -> red rad: [255, 0, 0], green: "#00ff00", blue: [0, 0, 255] };
所以我們可能會為 palette 定義一個類型,這樣錯別字就會被檢測出來了:
type Colors = "red" | "green" | "blue"; type RGB = [red: number, green: number, blue: number]; const palette: Record = { rad: [255, 0, 0], // ~~~~ The typo is now correctly detected green: "#00ff00", blue: [0, 0, 255] };
但是這時候我們再調用 palette.red 的方法,你會發現 TS 的類型推斷會出錯:
type Colors = "red" | "green" | "blue"; type RGB = [red: number, green: number, blue: number]; const palette: Record = { red: [255, 0, 0], green: "#00ff00", blue: [0, 0, 255] }; // 'palette.red' "could" 的類型是 string | RGB ,所以它不一定存在 at 方法 const a = palette.red.at(0);
這就讓我們陷入了兩難的境地,我們用更嚴格了類型約束了寫出 bug 的可能性,但是卻失去了類型推斷的能力。
satisfies 關鍵字就是用來解決這個問題的,它既能讓我們驗證表達式的類型是否與某個類型匹配,也可以保留基于值進行類型推斷的能力:
type Colors = "red" | "green" | "blue"; type RGB = [red: number, green: number, blue: number]; const palette = { rad: [255, 0, 0], // 可以捕獲到錯別字 rad green: "#00ff00", blue: [0, 0, 255] } satisfies Record; // 都可以調用 const a = palette.red.at(0); const b = palette.green.toUpperCase();
in 操作符類型收窄優化
在日常開發中,我們經常需要處理一些在運行時不完全確定的值,比如我們現在有下面兩個類型:
interface Duck { quack(): string; } interface Cat { miao(): string; }
在實際使用過程中,TS 不能確定 value 是否是上面中哪一個類型,所以會拋出錯誤:
function main(value: Duck | Cat) { if (value.quack) { // roperty 'quack' does not exist on type 'Duck | Cat'. return value.quack; } }
我們可能會使用 in 這樣的關鍵字來實現簡單的類型收窄:
function main(value: Duck | Cat) { if ('quack' in value) { return value.quack; } }
也可以實現一個更通用的類型守衛,可以參考我這篇文章:什么是鴨子類型?
但是,這個寫法的前提是我們用到的對象有明確的類型,如果這個對象的屬性沒有明確的類型呢?我們來看看下面這段 JAVAScript 代碼:
function tryGetPackageName(context) { const packageJSON = context.packageJSON; // 檢查是否是個對象 if (packageJSON && typeof packageJSON === "object") { // 檢查是否存在一個字符串類型的 name 屬性 if ("name" in packageJSON && typeof packageJSON.name === "string") { return packageJSON.name; } } return undefined; }
把這段代碼重寫為規范的 TypeScript,我們只需要定義一個 Context 類型,但是由于 packageJSON 沒有明確的類型定義,再使用 in 進行類型收窄就有問題了:
interface Context { packageJSON: unknown; } function tryGetPackageName(context: Context) { const packageJSON = context.packageJSON; // 檢查是否是個對象 if (packageJSON && typeof packageJSON === "object") { // 檢查是否存在一個字符串類型的 name 屬性 if ("name" in packageJSON && typeof packageJSON.name === "string") { // ~~~~ // error! Property 'name' does not exist on type 'object. return packageJSON.name; // ~~~~ // error! Property 'name' does not exist on type 'object. } } return undefined; }
這是因為 in 操作符只會嚴格收窄到實際定義被檢查屬性的類型,所以 packageJSON 的類型從 unknown 收窄到了 object ,而 object 類型上不存在 name 屬性,就會引發報錯。
TypeScript 4.9 優化了這個問題,in 操作符更加強大了,它會被收窄為被檢查類型和 Record<"property-key-being-checked", unknown> 的交叉類型。。。
比如在上面的例子中,packageJSON 的類型會被收窄為 object & Record<"name",unknown>,這樣我們直接訪問 packageJSON.name 就沒問題了!
interface Context { packageJSON: unknown; } function tryGetPackageName(context: Context): string | undefined { const packageJSON = context.packageJSON; // 檢查是否是個對象 if (packageJSON && typeof packageJSON === "object") { // 檢查是否存在一個字符串類型的 name 屬性 if ("name" in packageJSON && typeof packageJSON.name === "string") { // 可以正常運行! return packageJSON.name; } } return undefined; }
TypeScript 4.9 還加強了一些關于如何使用 in 操作符的檢查,比如左側要檢查的屬性必須是 string | number | symbol 類型,而右側類型必須要可分配給 object。accessor 關鍵字支持
accessor 是 ECMAScript 中即將推出的一個類關鍵字,TypeScript 4.9 對它提供了支持:
class Person { accessor name: string; constructor(name: string) { this.name = name; } }
accessor 關鍵字可以為該屬性在運行時轉換為一對 get 和 set 訪問私有支持字段的訪問器:
class Person { #__name: string; get name() { return this.#__name; } set name(value: string) { this.#__name = name; } constructor(name: string) { this.name = name; } }
NaN 相等判斷警告
NaN 是一個特殊的數值,代表 “非數字” ,在 JS 中它和任何值相比較都是 false,包括它自己:
console.log(NaN == 0) // false console.log(NaN === 0) // false console.log(NaN == NaN) // false console.log(NaN === NaN) // false
相對應的,所有值都不等于 NaN:
console.log(NaN != 0) // true console.log(NaN !== 0) // true console.log(NaN != NaN) // true console.log(NaN !== NaN) // true
這其實并不是 JavaScript 特有的問題,因為任何包含 IEEE-754 浮點數的語言都有相同的行為;但 JavaScript 的主要數字類型就是浮點數,并且 JavaScript 中的數字解析為 NaN 還挺常見的,所以在代碼中去比較值是否等于 NaN 的情況還挺普遍的。但是正確的做法應該是使用 Number.isNaN 函數來判斷。假如你不知道這個問題,就可能引發一些 bug。
在 TypeScript 4.9 中,如果你直接用一些值和 NaN 相比較,會拋出錯誤并提示你使用 Number.isNaN:
function validate(someValue: number) { return someValue !== NaN; // ~~~~~~~~~~~~~~~~~ // error: This condition will always return 'true'. // Did you mean '!Number.isNaN(someValue)'? }
return 關鍵字的定義
在編輯器中,當你對 return 關鍵字運行 go-to-definition 時,TypeScript 現在會自動跳轉到相應函數的頂部。這有助于我們快速了解 return 屬于哪個函數。
另外,TypeScript 會將此功能擴展到更多關鍵字,例如 await、yield、switch、case、default 等等。
最后
TypeScript 團隊最近還發布了 5.0 版本的迭代規劃(https://github.com/microsoft/TypeScript/issues/51362),這將是 TypeScript 的又一個大的版本,其中包含了很多有趣的想法,還是挺值得期待的!
更多詳細更新請查看 TypeScript 官方博客:https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-rc/
你覺得上面哪些更新對你最有用呢?歡迎在評論區和我留言;如果這篇文章幫助到了你,歡迎點贊和關注。