iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 14
2
自我挑戰組

Typescript 初心者手札系列 第 14

【Day 14】TypeScript 資料型別 - 複合型別(Union & Intersection) & 型別檢測(Type Guard)

  • 分享至 

  • xImage
  •  

閱讀今天的文章前,先回顧一下昨天的學習,回答看看:

  • 字面值型別支援哪幾種型別?
  • 型別別名(Type Alias)的運作機制為何?

如果有點不清楚答案的話,可以看看 Day13 的文章喔!

今天來看看之前已經出現過很多次的複合型別。複合型別又可以分為聯合型別(Union Type)交集型別(Intersection Type) ,這類型的型別由邏輯運算子組成,分別是 | 與 & 。

還記得 JS 中的邏輯運算子嗎?| 是 OR ,而& 則是 AND 的概念。在 TS 中,聯合型別(Union Type)和交集型別(Intersection Type) 正是應用此概念的型別。

聯合型別(Union Type)

聯合型別用在資料可以是多種型別中的一種,舉個最簡單的例子來說:

let sign : string | number

sign = 0 //OK
sign = 'red' //OK
sign = true //Error:Type 'true' is not assignable to type 'string | number'

sign 這個變數允許是字串或是數字兩種型別,只要符合其中一種即可,但若是其他型別就會報錯(預設設定下,undefined 和 null不會報錯,除非 strictNullChecks 為 true)。

聯合型別的屬性或方法

當 TS 不確定聯合型別的變數到底是哪個型別的時候,只能使用此聯合型別所有型別共有的屬性或方法,否則會報錯,舉例來說:

function getLength(something: string | number): number {
    return something.length;
}

//Error: Property 'length' does not exist on type 'string | number'. Property 'length' does not exist on type 'number'.

上面的範例,參數 something 可以是字串或是數字,但由於 .length 不是字串和數字的共有屬性,數字型別無法使用,所以會報錯。但如果是字串和數字的共同屬性或方法 toString()就可以

function getString(something: string | number): string {
    return something.toString();
}

另外,聯合型別的變數在被賦值的時候,也會根據型別推論的規則推斷出一個型別:

let myFavorite: string | number;
myFavorite = 'dog';
console.log(myFavorite.length); // 3
myFavorite = 4;
console.log(myFavorite.length); // Error : Property 'length' does not exist on type 'number'.

上面的範例中,第二行的 myFavorite 被推論成字串,使用 length 屬性不會報錯,而第四行的 myFavorite 被推論成數字,使用 length 屬性會報錯。

事實上,聯合型別很適合用在不同型別的狀況下,但若只能使用共同屬性或方法是很困擾的。因此,通常碰到聯合型別,大部分會型別檢測(Type Guard)來限縮操作的資料型別,避免 TS 報錯。

又來了一個新名詞!先來看看Type Guard 型別檢測到底是什麼吧?

型別檢測 Type Guard

型別檢測是在執行時可以限縮型別的表達式,通常會在條件式(例如if...else 語句)中進行檢查後。下面是常見的幾種方式:

  1. 使用 JS 運算子:typeof 、 instanceof 或 isArray 等
  2. 使用使用者自定義的型別檢查:型別謂語(type predicates)、in 關鍵字

JS 運算子:

  1. typeof 用來檢測未經計算的資料型別。在使用 typeof 運算子時,有幾個型別要特別小心,分別是原始型別的 null 以及陣列,兩者判斷後的結果都是物件。
    範例:函式 foo 傳入的參數可以是字串或數字,但在操作時可以透過型別檢測判斷是否為字串,若是就印出參數的長度; 若不是,就印出參數。
function foo(bar: string | number) {
 if(typeof bar === 'string') {
   console.log(bar.length)
 }
 // 不是字串? 此時,TS 會聰明的推論 bar為數字 
   console.log(bar)
 }
 
 foo('hello')    //5 
 foo(123)        //123
  1. instanceof 用來檢測建構函式 A 的 prototype 是否出現在某個 instance 物件 B 的原型鍊上,簡單來說,就是用来判斷 A 是否為 B 的實例。表達式為 A instanceof B 。範例程式碼如下:
class Song {
 constructor(public title: string, public duration: number) {}
}

class Playlist {
 constructor(public name: string, public songs: Song[]){}
}

function getItemName(item: Song | Playlist) {
 if(item instanceof Song) {
   return item.title;
 }
 return item.name;
}

getItemName(new Song('Wonderful', 300)) //  'Wonderful'

使用者自定義:

  1. 型別謂語: 是一個具特殊回傳值的函式,它會向編譯器傳遞訊號,告知回傳值的型別。型別謂語只能用在單一傳入值,且會回傳布林值。下面的範例中型別謂語也就是 s is string,語法為參數名 is 型別
function isFish(pet: Fish | Bird): pet is Fish {
   return (pet as Fish).swim !== undefined;
}
  1. in 關鍵字: 使用 in 關鍵字會回傳布林值,判斷屬性是否存在特定物件或其原型鏈上,語法為 n in x,n 為字串或字串字面值,x 為 union型別
function isSong(item: any): item is Song {
 return 'title' in item;
}

複雜的聯合型別

倘若現在有自定義的三個型別 Info1 、 Info2和 Info1 及 Info2 聯集的UnionInfo,也就是說 UnionInfo 可以是 Info1 或是 Info2。

type Info1 ={
    name: string,
    age: number
}

type Info2 = {
    isGirl : boolean,
    nickname: string 
}

type UnionInfo = Info1 | Info2

讓我們來測試一下 TS 怎麼判斷 UnionInfo 型別的呢?

// 1.含部分Info1,含部分Info2 => Error
let person1 : UnionInfo ={
    name: 'Una',
    isGirl: true
}

//2. 含全部Info1,不含Info2 => PASS!
let person2 : UnionInfo ={
    name : 'Una',
    age: 18 
}

//3. 含全部Info2,不含Info1 => PASS!
let person3 : UnionInfo ={
    isGirl : true,
    nickname: 'Nana'
}

//4. 含全部Info1,部分Info2 => PASS!
let person4 : UnionInfo ={
    name : 'Una',
    age: 18 ,
    isGirl : true,
}

//5. 含全部Info2和全部Info1  => PASS!
let person5 : UnionInfo ={
    name : 'Una',
    age: 18,
    isGirl : true,
    nickname: 'Nana'
}

從結果可以知道,變數裡的值只要「完全」滿足 Info1 或 Info2 其中一種型別即可,但若只滿足 Info1 或 Info2 的部分屬性仍然會報錯。

交集型別(Intersection Type)

交集型別主要用來將多個型別合併成一個型別。舉例來說:Person & Contact 代表資料為此型別的變數必須擁有兩者的所有屬性。

type Person = {
    name: string;
}

type Contact = {
    phone: string;
}

function showPersonContact(personContact: Person & Contact): void {
    console.log(personContact)
}

let personContact: Person & Contact = {name: "Dane", phone: "111-111-111"};
showPersonContact(personContact); //{name: "Dane", phone: "111-111-111"}

複雜的交集型別

倘若和上面的聯合型別一樣,定義了三個型別 Info1 、 Info2和 Info1 及 Info2 交集的 IntersecInfo。

type Info1 ={
    name: string,
    age: number
}

type Info2 = {
    isGirl : boolean,
    nickname: string 
}

type IntersecInfo = Info1 & Info2

同樣的狀況來測試一下 TS 會如何判斷:

// 1.含部分Info1,含部分Info2 => Error
let person1 : IntersecInfo ={
    name: 'Una',
    isGirl: true
}
//2. 含全部Info1,不含Info2 => Error
let person2 : IntersecInfo ={
    name : 'Una',
    age: 18 
}

//3. 含全部Info2,不含Info1 => Error
let person3 : IntersecInfo ={
    isGirl : true,
    nickname: 'Nana'
}

//4. 含全部Info1,部分Info2 => Error
let person4 : IntersecInfo ={
    name : 'Una',
    age: 18 ,
    isGirl : true,
}

//5. 含全部Info2和全部Info1  => PASS!
let person5 : IntersecInfo ={
    name : 'Una',
    age: 18,
    isGirl : true,
    nickname: 'Nana'
}

這時候會發現變數必須含有所有的 Info1 和 Info2 才能通過 TS,也再次驗證了上面提到 TS 中交集(intersection) 的用法是將多個資料型別結合為一個的概念。簡單來說,如果我們註記 IntersecInfo 在某變數上,該變數必須實踐 name、age、isGirl 跟 nickname 這四種屬性否則就會報錯。

小傑

今天理解了複合型別中的聯合型別(Union)和交集型別(Intersection),兩者的概念想成 JS 中的邏輯運算子 |(OR)&(AND),其實不難掌握核心觀念,但實際開發應用上感覺仍有很多眉眉角角和雷區,或許等之後碰到再來補充吧!另外,型別檢測(Type Guard)初步了解幾種方式和寫法,但實際應用層面仍然是團迷霧,之後會再細細研究補充。

參考資料:
TS 官網 - Advanced Type
User defined Type Guards in TypeScript
Understanding TypeScript: typeof Type Guard
Understanding TypeScript: instanceof Type Guard
TypeScript Literal Type Guards and in Operator


上一篇
【Day 13】TypeScript 資料型別 - 字面值型別(Literal Types) & 型別別名(Type Alias)
下一篇
【Day 15】TypeScript 資料型別 - 特殊型別(上)- Never
系列文
Typescript 初心者手札30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
sa0124
iT邦新手 5 級 ‧ 2019-11-13 01:06:16

一個小地方:

let myFavorite: string | number;
myFavorite = 'dog';
console.log(myFavorite.length); // 5 ==> 這裡應該是 3 才對

謝謝版主的文章~

Kira iT邦新手 5 級 ‧ 2019-12-30 16:28:52 檢舉

謝謝揪錯!

我要留言

立即登入留言