บางคนที่เขียน 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 ได้ … โดยผมจะใช้วิธีคิดแบบนี้

  1. ผมเลือกหยิบเอามาเฉพาะฟิลด์ที่ผมต้องการด้วยการใช้ Pick
  2. ผมจะทำให้ฟิลด์อื่นๆนอกจากนั้นเป็น 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 มาก ถ้าเรารู้จักประยุกต์ใช้

จอบอ … ใครมีวิธีง่ายกว่านี้บอกผมด้วย