Union Types และ Intersection Types

มีบ่อยครั้งที่ Type ที่สร้างขึ้นมาจะไม่ครอบคลุมการใช้งานได้ทั้งหมด หรือมีความต้องการที่จะใช้ตัวแปรเก็บค่าที่มีความซับซ้อนมากขึ้น เราสามารถใช้ Union Types และ Intersection Types มาช่วยได้

Union Types

สำหรับการ Union เคยเกริ่นไปแล้วในบทก่อน (Literal Types) คือการใช้ | เพื่อเชื่อม Type เข้าด้วยกัน ทำให้ Type ที่สร้างขึ้นใหม่มีความยืดหยุ่นมากขึ้น

type ID = string | number // ID สามารถเป็น string `หรือ` ตัวเลขก็ได้
const a: ID = "abc"
const b: ID = 101

เราจะนำ Union ไปเขียนเป็น Inline ในฟังก์ชั่นก็ได้เช่นกัน

function sayHello(people: string | string[]) { // รับ string หรือชุดอาเรย์ของ string
...
}

เมื่อใช้ Union แล้วต้องการใช้ตัวแปรนั้น จะต้องทำการเช็ค Type ก่อนในโค้ด เรียกว่าการ Narrowing เช่นตามตัวอย่าง people อาจเป็น String หรือ Array ก็ได้ ถ้าไม่เช็คแล้วใช้ Function กับตัวแปรนั้น อาจมี Error เกิดขึ้นได้

function sayHello(people: string | string[]) {
console.log(`สวัสดี ${people.join(", ")}`)
}
sayHello(["เจน", "นุุ่น"]) // ✅ สวัสดี เจน, นุ่น
sayHello("โบว์") // ❌ Error
ไม่สามารถใช้ join() ได้ เพราะถ้า people เป็น String จะเกิด Runtime error

จากตัวอย่างข้างต้น ต้องทำการแก้ไขโค้ดให้รองรับทุก Type ที่ Union กัน เช่น

function sayHello(people: string | string[]) {
if (typeof people == "string") {
console.log(`สวัสดี ${people} มาคนเดียวเหรอ`)
} else {
console.log(`สวัสดี ${people.join(", ")}`)
}
}
sayHello(["เจน", "นุุ่น"]) // ✅ สวัสดี เจน, นุ่น
sayHello("โบว์") // ✅ สวัสดี โบว์ มาคนเดียวเหรอ

อีกตัวอย่างหนึ่งที่จะได้ใช้ Union Type เช่น Literal Types

type Color = "red" | "green" | "blue"
function printColorCode(c: Color): string {
switch (c) {
case "red":
return "#FF0000"
case "green":
return "#00FF00"
case "blue":
return "#0000FF"
}

ถ้าในฟังก์ชั่นนี้ไม่ได้เช็คทุกกรณี เช่นมี case red กับ green แต่ไม่มี blue หรือสะกดเคสใดๆ ผิด จะเกิด Error ขึ้น

Error เพราะพิมพ์ "blue" ผิด

จะเห็นได้ว่า TypeScript จะทำให้เราเขียนโค้ดที่ Bug-free มากขึ้น เนื่องจากเรารองรับ Edge Case ท้ังหมดได้จากการจำกัด Type ที่จะใช้ในฟังก์ชั่น แต่ก็ยังมีความยืดหยุ่นจากการ Union ได้ ทำให้ไม่ต้องเขียนแยกกัน ทำให้มีโค้ดมากเกินความจำเป็น

Intersection Types

การใช้ Intersection จะใช้กับ Interface จะเป็นเสมือนการ "ต่อ" Interface ต่างๆ เข้าด้วยกัน แต่เงื่อนไขจะแตกต่างกับ Union คือ Intersection นั้นค่าจะต้องเป็นออบเจกต์ที่มีหน้าตา ตรงกันกับทุก Type ที่นำมา Intersect กัน

สมมุติว่ามี Interface ที่เป็นรูปวงกลม กับสี่เหลี่ยมที่มีค่าสี และคำนวณพื้นที่ได้

interface ColorfulCircle {
color: string
radius: number
area: () => number
}
interface ColorfulSquare {
color: string
size: number
area: () => number
}

สังเกตว่ามีการใช้ชื่อและ Type ของ color และ area ซำ้กัน ซึ่งถ้าเราใช้ความสามารถของ Intersection (ตัว &) เราจะแยก Interface ที่ใช้ซำ้กันออกมา แล้ว Intersect ทีหลังได้ แบบนี้

interface Colorful {
color: string
}
interface Circle {
radius: number
}
interface Square {
size: number
}
interface Measurable {
area: () => number
}
// มี color, radius และฟังก์ชั่น area
type ColorfulCircle = Colorful & Circle & Measurable
// มี color, size และฟังก์ชั่น area
type ColorfulSquare = Colorful & Square & Measurable

การใช้ Intersection จะทำให้เราสร้าง Interface เล็กๆ แล้วเอามา Reuse ต่อได้ ทำให้ Code อ่านง่ายขึ้นด้วย