今天的文章分成三個子題,會先從Type Assertion (型別斷言) 的基本語法開始看起,然後再一起討論兩個跟Type Assertion相關的 non-null assertion operator ( !
)和 const assertion。
Type assertion的作用是告訴TypeScript compiler某個變數的型別是我所聲稱的型別,並會忽略compiler推導的型別。
雖然看似有點像Type casting(型別轉換),而且也有很多討論會說type assertion是將某個變數轉型(casting)為其他型別,但實際上type assertion並不會轉換變數的型別。
Type assertion的目的是方便開發者告知compiler變數的型別,讓程式能通過型別檢查,避免某些時候compiler沒辦法正確地推導出開發者需要的型別而出現錯誤。
假設有些時候會需要先給一個空物件,然後等到程式跑完或是使用者輸入完相關資訊才會給予物件屬性值:
let user = {};
user.id = 12345678; // error
user.name = "Bon"; // error
Property 'id' does not exist on type '{}'.(2339)
Property 'name' does not exist on type '{}'.(2339)
給予user物件兩個屬性和屬性值後,compiler可能會因為無法判斷 {}
的型別(應該包含哪些屬性)而出現錯誤。
遇到這個問題時,第一時間可能會認為那就以type annotation的方式,明確指定 user 物件的型別是 User:
interface User{
id: number;
name: string;
}
let user: User = {}; // type annotation
user.id = 12345678;
user.name = "Bon";
結果出現
Type '{}' is missing the following properties from type 'User': id, name(2739)
因為type annotation是讓變數一定會是這個型別(而且永遠都是這個型別),當user空物件事先被註記為是User型別,卻不包含User型別應該要有的 id
和 name
屬性,所以compiler又丟出錯誤訊息了...
這種時候就可以用type assertion的方式向TypeScript compiler斷言:
「嘿!我這個物件其實是User型別」
讓compiler不會因為變數不符合compiler自己推導的型別而拋出錯誤訊息。
Type assertion聲稱型別的方式有兩種,一種是用角括號 <>
指定型別:
// 1. <>
interface User{
id: number;
name: string;
}
let user = <User>{}; // type assertion
user.id = 12345678;
user.name = "Bon";
但要注意 <>
語法無法在 .tsx
檔案使用,也就是React的檔案中使用,因為會被以為是JSX語法而出現錯誤。
所以有另一種方法是用 as
關鍵字:
// 2. as
interface User{
id: number;
name: string;
}
let user = {} as User; // type assertion
user.id = 12345678;
user.name = "Bon";
還有幾個關於type assertion的事項可以提一下:
let oneStr = "1" as number; // error
let foo = (express as any) as T;
null
,例如以下範例的user物件少給了一個屬性:interface User{
id: number;
name: string;
}
let user = {} as User;
user.id = 12345678; // no error!!!!!
因為第3點的關係,有些人認為type assertion削弱了TypeScript原本強調的type safty (型別安全性),要儘量避免使用type assertion。
第3點相關的案例和討論可以參考以下兩篇文章:
!
運算子是當開發者確定變數值有不會是 null
或 undefined
的情況時使用。
例如下面範例刻意加上了一個 toString
方法,而且因為有預設值 "enter message"
,所以型別允許有 null
的情況:
function showMessage({msg="enter message"}: {msg?: string | number | null} = {}) {
alert(msg.toString());
}
showMessage();
不過這個狀況下,compiler認為如果輸入參數是 null
的時候不會有 toString
方法,就會報錯:
'msg' is possibly 'null'.(18047)
因為有預設值,所以這時候可以加上 !
,向 compiler 斷言輸入值不會有是 null
情況:
function showMessage({msg="enter message"}: {msg?: string | number | null} = {}) {
alert(msg!.toString());
}
showMessage(); // hi
終於到最後一個子題 ─ const assertion。
Const assertion其實就是前面Type Assertion的特例,可以對literal value 斷言為 const
。
根據官方的const assertion定義,const assertion語法如下,而且會有如註解的效果:
// 1. x 的 type 會是 "x" 且變成 const 變數
let x = "x" as const;
const y = "y" // y是string型別
// 2. arr 變成 readonly tuple
const arr = [9, 10] as const;
// 3. nine 和 ten 都是 readonly 屬性
const obj = {nine: 9, ten: 10} as const;
而React .jsx
檔案以外的地方同樣可以用 <>
寫成:
// 1.
let x = <const>"x";
// 2.
const arr = <const>[9, 10];
// 3.
const obj = <const>{nine: 9, ten: 10};
在案例1,和建立const變數 const y = "y"
不一樣的是,變數x的型別會是"x",而變數y的型別則是string。
如果是對陣列(array)作const assertion,則案例2中的陣列arr會變成只有兩個元素9和10且只能讀取元素值的tuple。
個人認為const assertion最好用的地方是在對物件(object)作const assertion。和JavaScript語法一樣,在TypeScript建立一般的物件常數 const obj
,其實只是不能改變物件參考的記憶體位址,而記憶體位址指向的物件內容還是可以修改的:
const obj = {nine: 9, ten: 10};
obj.nine = 99; // ok
但如果是對物件作const assertion,則該物件的屬性等於是都加上 readonly
,也就無法修改物件的屬性了:
const obj = {nine: 9, ten: 10} as const;
obj.nine = 99; // error: Cannot assign to 'nine' because it is a read-only property.(2540)
最後補充一篇stackoverflow的問答,這位網友有舉一個例子說明「對array作const assertion,能避免rest parameter可能會出現的錯誤」,有興趣的人可以看看這位網友的例子。
參考資料
Type Assertions @TypeScript Handbook
Type Assertions @TypeScript Tutorial
Explain Type assertions in TypeScript @GeeksforGeeks
Non-null Assertion Operator (Postfix!) @TypeScript Handbook
const assertions @TypeScript 3.4 relaease note