TypeScript - Pick and Partial Built-in Type Combo
บางคนที่เขียน TypeScript มาสักพักนึงคงเคยเจอ built-in type … Pick และ Partial ผ่านตามาบ้าง … แต่บางทีเราก็ไม่รู้ว่ามันจะเอาไปใช้ในสถานการณ์ไหนได้บ้าง … วันนี้ผมมีท่านึงในการใช้คอมโบ Pick + Partial มาเสนอ ซึ่งเป็นท่าที่ช่วยให้ type ของเรายืดหยุ่นมากขึ้น แต่ก็ยังคงความถูกต้องเอาไว้เหมือนเดิม
มาเริ่มกัน
สมมุติผมมี interface แบบข้างล่างนี้
interface Animal {
legs: number;
wings: number;
canWalk: boolean;
canFly: boolean;
canSwim: boolean;
canSpeak: boolean;
}
ผมอยากได้ฟังก์ชั่นที่สามารถเช็คได้ว่า object นี้เป็นนกหรือไม่ ผมสามารถเขียนฟังก์ชั่นแบบนี้
/**
* Check if the given object is bird
* It must has 2 legs, 2 wings, can walk and can fly
*
* @return boolean
**/
function isBird(obj: Animal) {
return obj.legs === 2 obj.wings === 2 && obj.canWalk && obj.canFly;
}
ตอนเอาไปใช้ ผมหวังว่าเราจะสามารถเขียนได้แบบนี้
const myBird = {
legs: 2,
wings: 2,
canWalk: true,
canFly: true
}
isBird(myBird);
แต่มันทำไม่ได้ … compiler ก็จะด่าเรากลับมา เพราะฟังก์ชั่นถูกเขียนไว้ว่าต้องเอา Animal
ส่งเข้าไปเช็ค … แต่สิ่งที่ผมส่งเข้าไปมันไม่ครบตาม interface ของ Animal
งัย
ถ้าผมอยากให้มันเช็คได้ ผมต้อง define ทุกอย่างให้ครบแบบนี้
const myBird = {
legs: 2,
wings: 2,
canWalk: true,
canFly: true,
canSwim: false,
canSpeak: false
}
isBird(myBird);
แต่ ใครจะไปสน … ผมไม่สนเว้ยยย ผมมีคุณสมบัตินกแบบของผม ผมขอแค่มันมี 2 ขา 2 ปีก เดินได้ บินได้ ผมก็ถือว่ามันเป็นนกแล้ว … บางตัวมันอาจจะพูดได้ ว่ายน้ำได้ ก็เรื่องของมัน … แต่ถ้ามันตรงตามที่ผมบอกผมก็ถือว่ามันเป็นนก!!
ทีนี้ผมจะทำยังงัยที่จะทำให้ฟังก์ชั่น isBird
สามารถรับ object ที่มีรูปร่างแบบ Animal
แต่ไม่จำเป็นต้องระบุคุณสมบัติครบทั้งหมด ขอแค่มีส่งที่ผมต้องใช้เช็คให้ครบก็พอ
ในเคสแบบนี้ ผมสามารถใช้ Partial
ร่วมกันกับ Pick
เพื่อสร้าง type ใหม่ขึ้นมาจาก Animal
ได้ … โดยผมจะใช้วิธีคิดแบบนี้
- ผมเลือกหยิบเอามาเฉพาะฟิลด์ที่ผมต้องการด้วยการใช้ Pick
- ผมจะทำให้ฟิลด์อื่นๆนอกจากนั้นเป็น optional ด้วยการใช้ Partial
ถ้าทำแบบนี้ได้ ก็จะตรงกับ สิ่งที่เราต้องการ
สำหรับข้อ 1 ผมสามารถเขียนได้แบบนี้
type BirdSpec = Pick<Animal, 'legs' | 'wings' | 'canWalk' | 'canFly'>;
function isBird(obj: BirdSpec) {
return obj.legs === 2 obj.wings === 2 && obj.canWalk && obj.canFly;
}
แบบนี้ object ที่ผมส่งเข้าไป ต้องมี ของ 4 อย่างตามที่ผมระบุ และ type ของแต่ละฟิลด์ข้างในต้องตรงกับที่ประกาศใน Animal
หรือพูดอีกอย่างหนึ่งว่า หน้าตาของ BirdSpec
จะมีค่าเทียบเท่า
interface BirdSpec {
legs: number;
wings: number;
canWalk: boolean;
canFly: boolean;
}
ข้อดีของการใช้ Pick
แทนการประกาศ BirdSpec
เป็น interface แยกไปอีกตัว คือ เราไม่ต้อง maintain ของแยกกัน ทั้ง Animal
และ BirdSpec
วันนึงเกิดผมต้องการเปลี่ยน type ของสักฟิลด์ใน Animal
เป็นอย่างอื่น เช่น ผมอาจจะเปลี่ยน legs เป็น string (สมมุตินะ) ผมก็แก้ที่ Animal
อย่างเดียว ไม่ต้องไปตามแก้ BirdSpec
ด้วย
แต่ข้อเสียก็คือ หากมีฟิลด์อื่นๆปนเข้ามา compiler มันก็จะด่าเราอยู่ดี เพราะมันรู้จักแค่ 4 ฟิลด์นี้ … ซึ่งสมมุติหากเราเอาสัตว์ชนิดอื่นที่ไม่ใช่นกมาเช็คมันก็อาจมี อย่างอื่นปนมาบ้างใช่ป่ะ เพราะฉะนั้นทำแค่นี้เลยยังไม่พอ
ทีนี้ก็ต้องใช้ Partial ช่วย
Partial จะช่วยให้ ทุกฟิลด์ใน interface กลายเป็น optional
สามารถเขียนได้แบบนี้
type MaybeAnimal = Partial<Animal>;
ซึ่งมันก็จะมีค่าเทียบเท่ากับการประกาศ interface ใหม่แบบนี้
interface MaybeAnimal {
legs?: number;
wings?: number;
canWalk?: boolean;
canFly?: boolean;
canSwim?: boolean;
canSpeak?: boolean;
}
หมายความว่า หากมีแค่บางฟิลด์ในนี้ compiler จะปล่อยให้ผ่าน … แต่ถ้ามีฟิลด์แปลกปลอมปนเข้ามามันก็ด่าทันที หากฟิลด์นั้นมันไม่รู้จักใน type นี้
ทีนี้หาก นำมาใช้ร่วมกัน ผมจะเขียน BirdSpec ใหม่ได้แบบนี้
type BirdSpec = Pick<Animal, 'legs' | 'wings' | 'canWalk' | 'canFly'> & Partial<Animal>;
BirdSpec
ตอนนี้ก็จะมีค่าเทียบเท่ากับ interface ข้างล่างนี้ทันที
interface BirdSpec {
legs: number;
wings: number;
canWalk: boolean;
canFly: boolean;
canSwim?: boolean;
canSpeak?: boolean;
}
หมายความว่า object นั้น ต้องมี legs
, wings
, canWalk
, canFly
ตาม type ที่ประกาศใน Animal
… และสามารถมีฟิลด์อื่นใน Animal
ปนเข้ามาได้ด้วย แต่หากใส่ฟิลด์อื่นๆที่นอกเหนือจากที่ Animal
รู้จัก compiler ก็จะด่าเราทันที
อ่านยากจัง มีสั้นๆกว่านี้มั้ย
ยังไม่สะใจ? ไม่เป็นไร เราสามารถสร้าง Generic Type ขึ้นมาใหม่ โดยใช้สิ่งที่เราเรียนรู้มาจากข้างบน
สามารถเขียนได้แบบนี้
export type RequiredSome<T, K extends keyof T> = Pick<T, K> & Partial<T>;
ทีนี้ BirdSpec เราจะเหลือสั้นๆแค่นี้
type BirdSpec = RequiredSome<Animal, 'legs' | 'wings' | 'canWalk' | 'canFly'>;
ใครมีชื่อดีกว่า RequiredSome
มะ -0-
จะเห็นว่า built-in type พวกนี้มัน powerful มาก ถ้าเรารู้จักประยุกต์ใช้
จอบอ … ใครมีวิธีง่ายกว่านี้บอกผมด้วย