跳至主要内容

[ Day-003 ] TypeScript 型別限縮 Narrowing

使用 type guards 做型別限縮 Narrowing

Type Guards

Type Guards 可以讓 TypeScript 在當程式碼出現分支 branch 註解1
可以進行型別的限縮 Narrowing
原先 Type Guards 用在 JavaScript 時可以使用 typeof 去判定任一變數型別 但卻只能生效在 runtime
而如果在 TypeScript 使用 typeof 去判定型別時
就有能力提供更多資訊給 TypeScript 在 compile 時期就能知道型別差異

Type Guards
typeof 'apple' === 'string';

typeof 100 === 'number';

typeof 9007199254740991n === 'bigint';

typeof true === 'boolean';

typeof Symbol('foo') === 'symbol';

typeof undefined === 'undefined';

typeof null === 'object'; // 特別容易混淆部分

typeof {} === 'object';

typeof [] === 'object'; // 特別容易混淆部分不是 array 是 object

typeof new Map() === 'object'; // 特別容易混淆部分

typeof new Set() === 'object'; // 特別容易混淆部分

function hello() {}

typeof hello === 'function';

const world = () => {};

typeof world === 'function';

Narrowing Case

了解 Type Guards 有哪些選項可以使用後
可以實際來看一個在 TypeScript 上使用的案例

原先有一函式 padLeft 可以傳入名為 padding 而型別是 number 或 string
如果在下面實作當中
TypeScript 將會 compile error
原因是 String.repeat 註解2 本身只接收型別為 number 的 count 參數
但在此案例下 padding 卻有可能不是 number 而是 string

function padLeft(padding: number | string, input: string): string {
return ' '.repeat(padding) + input;
}

所以在上面案例當中 我們需要使用 Type Guards
去幫我們在程式碼的建出分支
用來限縮一部分的型別的情境

function padLeft(padding: number | string, input: string) {
if (typeof padding === 'number') {
// 在這一區塊 padding 只會是 number 所以可以安心使用 String.repeat
return ' '.repeat(padding) + input;
}
// 在這一區塊 padding 只會是 string
// 因為 padding 不是 number 就是 string
// 只有兩種可能性
return padding + input;
}

Truthiness Narrowing

上面案例看完後
可以發現 typeof nullobject
那我們該如何 Narrowing null
我們可以透過 Truthiness Narrowing
在 JavaScript 當中包含以下 falsy

Boolean(null) === false;

Boolean(undefined) === false;

Boolean(false) === false;

Boolean(NaN) === false;

Boolean(0) === false;

Boolean(-0) === false;

Boolean(0n) === false;

Boolean('') === false;

Boolean(document.all) === false;

於是再來看個案例
在這個案例當中可以看見
typeof strs === 'object' 時 strs 有機會是 string[] 或是 null
然而 null 沒有辦法被 iterate 因此要 iterate strs 時就產生 compile error

function printAll(strs: string | string[] | null) {
if (typeof strs === 'object') {
//'strs' is possibly 'null'.
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
console.log(strs);
} else {
// do nothing
}
}

這時可以透過 Truthiness Narrowing 解決這問題
當中加上了 strs && 字段
讓進入 typeof strs === 'object' 分支當中的程式碼
不但要是 object 且至少還要是 truthy

function printAll(strs: string | string[] | null) {
if (strs && typeof strs === 'object') {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
console.log(strs);
}
}

補充內容

註解1

條件分支 (Conditional Branch):
使用 if 和 else 或 switch 來執行不同的程式路徑。

if (condition) {
// 執行這裡的程式碼 做些事情 branch #1
} else {
// 條件為假時執行這裡的程式碼 做其他事情 branch #2
}
switch (value) {
condition_1 :
// branch #1
break;
condition_2 :
// branch #2
break;
default:
// branch #3
break;
}

迴圈分支 (Loop Branch):
使用 if 和 else 來執行不同的程式路徑。

for (let i in [1, 2, 3, 4, 5]) {
// 執行這裡的程式碼 做些事情 branch #1
}
while (condition) {
// 執行這裡的程式碼 做些事情 branch #1
}

註解2

String.repeat 介面

    /**
* Returns a String value that is made from count copies appended together. If count is 0,
* the empty string is returned.
* @param count number of copies to append
*/
repeat(count: number): string;

參考連結