閱讀今天的文章前,先回顧一下昨天的學習,回答看看:
- 字面值型別支援哪幾種型別?
- 型別別名(Type Alias)的運作機制為何?
如果有點不清楚答案的話,可以看看 Day13 的文章喔!
今天來看看之前已經出現過很多次的複合型別。複合型別又可以分為聯合型別(Union Type)
和交集型別(Intersection Type)
,這類型的型別由邏輯運算子組成,分別是 | 與 & 。
還記得 JS 中的邏輯運算子嗎?|
是 OR ,而&
則是 AND 的概念。在 TS 中,聯合型別(Union Type)和交集型別(Intersection 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 語句)中進行檢查後。下面是常見的幾種方式:
- 使用 JS 運算子:typeof 、 instanceof 或 isArray 等
- 使用使用者自定義的型別檢查:型別謂語(type predicates)、in 關鍵字
JS 運算子:
- 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
- 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'
使用者自定義:
- 型別謂語: 是一個具特殊回傳值的函式,它會向編譯器傳遞訊號,告知回傳值的型別。型別謂語只能用在單一傳入值,且會回傳布林值。下面的範例中型別謂語也就是 s is string,語法為
參數名 is 型別
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined; }
- 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 的部分屬性仍然會報錯。
交集型別主要用來將多個型別合併成一個型別。舉例來說: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
一個小地方:
let myFavorite: string | number;
myFavorite = 'dog';
console.log(myFavorite.length); // 5 ==> 這裡應該是 3 才對
謝謝版主的文章~
謝謝揪錯!