Skip to main content

[TS] Type Guard and Narrowing

此篇為各筆記之整理,非原創內容,資料來源可見下方連結與文後參考資料。

Narrowing#

Narrowing 指的是將某一個可能為多種型別的變數,縮限成某單一種型別。

透過 Type Guards 可以讓 TypeScript 在 compile 階段,避免開發者使用到不屬於該型別的方法,並且可以作為「當型別為 ... 才 ... 的操作」。

使用 in#

in operator @ MDN

在 JavaScript 中,in 可以用來確保某一個屬性在特定的物件或其原型鏈中。

當某個變數的型別是其他型別的 Union 時,為了確保該變數的確是符合某個型別後,才能進行後續的操作:

"key" in x

  • 其中 x 是一個 Union Type 的「變數」(不是型別);key 則是預期在該型別內會有的屬性
  • 簡單想成 x 物件裡有屬性 "value"
type Cat = {
isMeow: boolean;
};
type Dog = {
isBark: boolean;
};
// Animal 有可能是 Cat 或 Dog
type Animal = Cat | Dog;
const getNoise = (animal: Animal) => {
// 使用 in 來做 Type Guard
// in 後加的是「變數」而不是「型別」
// 簡單想成是:animal 物件裡有屬性 "isBark",則一定是 Dog 型別
if ('isBark' in animal) {
console.log(animal.isBark); // 不會有 compile error
console.log('it is dog');
}
// 簡單想成是:animal 物件裡有屬性 "isMeow",則一定是 Cat 型別
if ('isMeow' in animal) {
// 已經斷言是貓,卻使用了 isBark 方法,compile 時就會報錯
console.log(animal.isBark); // Property 'isBark' does not exist on type 'Cat'.
console.log('it is cat');
}
};

使用 typeof 和 instances#

const isUnknown: unknown = 'hello, unknown';
// build in type guard: typeof
if (typeof isUnknown === 'string') {
// 這裡可以確保 isUnknown 是 string
isUnknown.split(', ');
}
// build in type guard: instanceof
if (isUnknown instanceof Promise) {
// 這裡可以確保 isUnknown 是 Promise
isUnknown.then((x: unknown) => x).catch((err: Error) => err);
}

Discriminated Unions & Unreachable Error#

Discriminated unions 是一種非常好用的方式,特別是在接收後端傳來的 response 時,我們常會需要根據某一個欄位的值來決定有其他哪些欄位,例如,status 是 OK 的話,則資料會帶有 payload,否則資料會帶有 error,則時候則可以利用 Discriminated Unions 的方式來達到。

Discriminated Unions 會定義多個不同的 Type,但這些 Type 中都會有一個共同的欄位,例如 status,以此欄位來判斷物件中會有其他哪些屬性。

// 定義多個不同的 Type,但這些 Type 中都會有一個共同的欄位,例如 `status`
// 以此欄位來判斷物件中會有其他哪些屬性。
interface ISuccessResp {
status: 'OK';
payload: unknown;
}
interface IErrorResp {
status: 'ERROR';
errorCode: number;
description: string;
}
// 利用 Union 的方式產生 Union Type
type Resp = ISuccessResp | IErrorResp;
const parseResponse = (resp: Resp) => {
switch (resp.status) {
case 'OK':
// 透過 narrow 可以確定這裡的 resp 是 ISuccessResp 的型別
return resp.payload;
case 'ERROR':
// 透過 narrow 可以確定這裡的 resp 是 IErrorResp 的型別
return resp.description;
default:
// 沒有判斷到的情況利用 exhaustiveness checking
// 避免有 case 在上面是沒有被定義到的
const _exhaustiveCheck: never = resp;
return _exhaustiveCheck;
}
};

User-defined type guards#

:parameterName is TYPE

  • 要定義 user-defined type guard,只需要定義一個函式,其「回傳值」的型別會是 type predicate,例如 value is string
  • type predicate 都會長像這樣 :parameterName is TYPE,而這個 parameterName 一定要是該函式中參數的名稱
  • user-defined type guards 一定是回傳 boolean,當回傳的 boolean 是 true 時,TypeScript 就會將變數的型別 narrow 到某一型別。
  • TypeScript 並不會管實際上在函式內做了什麼判斷,它只會辨認最終回傳的是 truefalse,如果是 true,表示符合該變數符合該 type predicate

使用 typeof#

/**
* user-defined type guards
* https://www.typescriptlang.org/docs/handbook/advanced-types.html#typeof-type-guards
*/
let isUnknown: unknown: "hello unknown";
function isNumber(x: any): x is number {
return typeof x === 'number';
}
function isString(x: any): x is string {
return typeof x === 'string';
}
if (isNumber(isUnknown)) {
// 這裡可以確保 isUnknown 是 number
isUnknown.toFixed();
}
if (isString(isUnknown)) {
// 這裡可以確保 isUnknown 是 string
isUnknown.split(', ');
}

透過 isFunction 來判斷某變數型別是否是 Function#

// https://exploringjs.com/tackling-ts/ch_type-guards-ion-functions.html#user-defined-type-guards
// 讓某個函式回傳的型別是 Function
// value is Function 即是 type predicate
function isFunction(value: unknown): value is Function {
// TypeScript 並不管實際上做了什麼判斷,只要回傳的值 true 即表示回傳的變數是 Function(type predicate)
return typeof value === 'function';
}

實作 typeof 的 user-defined type guard#

// https://exploringjs.com/tackling-ts/ch_type-guards-assertion-functions.html#a-first-attempt
function isTypeof<T>(value: unknown, prim: T): value is T {
if (prim === null) {
return value === null;
}
return value !== null && typeof prim === typeof value;
}

實作 isDefined 的 user-defined type guard#

// 如果 arg 沒加上 undefined,則 filtered 的型別會推導成 (string | undefined)[]
// 但若是使用 arg: T | undefined,則 undefined 會從該型別被抽出,因此最終會推導成 string[]
function isDefined<T>(arg: T | undefined): arg is T {
return typeof arg !== 'undefined';
}
const list = ['foo', 'bar', undefined];
const filtered = list.filter(isDefined);

程式範例#

What does the is keyword do in typescript? @ stackoverflow

// 程式碼來源:https://stackoverflow.com/a/45748366/5135452
// isString 是 Type Guard
type Cat = {
isMeow: boolean;
};
type Dog = {
isBark: boolean;
};
type Animal = Cat | Dog;
// Type Guard
function isDog(animal: Dog | Cat): animal is Dog {
return (animal as Dog).isBark === true;
}
const getNoise = (animal: Animal) => {
if (isDog(animal)) {
console.log(animal.isBark);
console.log('it is dog');
} else {
// console.log(animal.isBark); // 會報錯,不是 Dog 卻用了 dog 的方法
console.log(animal.isMeow);
console.log('it is cat');
}
};
const dog: Dog = {
isBark: true,
};
getNoise(dog);

Assertion-based type guards#

另外還有一種是 assert-based type guards,用來斷言該變數一定是某型別,否則會拋出錯誤:

function assertIsStringArray(arr: any[]): asserts arr is string[] {
if (!arr) throw new Error('not an array!');
const hasNonString = arr.some((item) => typeof item !== 'string');
if (hasNonString) throw new Error('not an array of string!');
}
const list = ['foo', 'bar', undefined];
assertIsStringArray(list); // throw error here
const arr = ['3', '2', '21'];
assertIsStringArray(arr); // 不會報錯,程式會繼續執行
arr.join(', ');
Last updated on