ไทย

คู่มือฉบับสมบูรณ์เกี่ยวกับ Mapped Types และ Conditional Types อันทรงพลังของ TypeScript พร้อมตัวอย่างการใช้งานจริงและกรณีศึกษาขั้นสูงสำหรับการสร้างแอปพลิเคชันที่แข็งแกร่งและปลอดภัยด้านไทป์

การเรียนรู้ Mapped Types และ Conditional Types ของ TypeScript อย่างเชี่ยวชาญ

TypeScript ซึ่งเป็นส่วนขยายของ JavaScript (superset) มีฟีเจอร์ที่ทรงพลังสำหรับการสร้างแอปพลิเคชันที่แข็งแกร่งและง่ายต่อการบำรุงรักษา ในบรรดาฟีเจอร์เหล่านี้ Mapped Types และ Conditional Types โดดเด่นในฐานะเครื่องมือที่จำเป็นสำหรับการจัดการไทป์ขั้นสูง คู่มือนี้จะให้ภาพรวมที่ครอบคลุมของแนวคิดเหล่านี้ โดยสำรวจ синтаксис (syntax), การใช้งานจริง และกรณีศึกษาขั้นสูง ไม่ว่าคุณจะเป็นนักพัฒนา TypeScript ที่มีประสบการณ์หรือเพิ่งเริ่มต้นเส้นทาง บทความนี้จะมอบความรู้ให้คุณเพื่อนำฟีเจอร์เหล่านี้ไปใช้อย่างมีประสิทธิภาพ

Mapped Types คืออะไร?

Mapped Types ช่วยให้คุณสามารถสร้างไทป์ใหม่ได้โดยการแปลงจากไทป์ที่มีอยู่เดิม โดยจะวนซ้ำไปตามคุณสมบัติ (properties) ของไทป์ที่มีอยู่และใช้การแปลงกับแต่ละคุณสมบัติ ซึ่งมีประโยชน์อย่างยิ่งสำหรับการสร้างรูปแบบต่างๆ ของไทป์ที่มีอยู่ เช่น การทำให้คุณสมบัติทั้งหมดเป็น optional หรือ read-only

Syntax พื้นฐาน

Syntax สำหรับ Mapped Type เป็นดังนี้:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

ตัวอย่างการใช้งานจริง

การทำให้ Properties เป็น Read-Only

สมมติว่าคุณมี interface ที่แทนโปรไฟล์ผู้ใช้:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

คุณสามารถสร้างไทป์ใหม่ที่ properties ทั้งหมดเป็น read-only:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

ตอนนี้ ReadOnlyUserProfile จะมี properties เหมือนกับ UserProfile แต่ทั้งหมดจะเป็นแบบ read-only

การทำให้ Properties เป็น Optional

ในทำนองเดียวกัน คุณสามารถทำให้ properties ทั้งหมดเป็น optional ได้:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfile จะมี properties ทั้งหมดของ UserProfile แต่แต่ละ property จะเป็น optional

การแก้ไขไทป์ของ Property

คุณยังสามารถแก้ไขไทป์ของแต่ละ property ได้อีกด้วย ตัวอย่างเช่น คุณสามารถแปลง properties ทั้งหมดให้เป็นสตริง:

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

ในกรณีนี้ properties ทั้งหมดใน StringifiedUserProfile จะมีไทป์เป็น string

Conditional Types คืออะไร?

Conditional Types ช่วยให้คุณสามารถกำหนดไทป์ที่ขึ้นอยู่กับเงื่อนไขได้ เป็นวิธีการแสดงความสัมพันธ์ของไทป์โดยพิจารณาว่าไทป์นั้นเป็นไปตามข้อจำกัดที่กำหนดหรือไม่ ซึ่งคล้ายกับตัวดำเนินการแบบ ternary (ternary operator) ใน JavaScript แต่ใช้สำหรับไทป์

Syntax พื้นฐาน

Syntax สำหรับ Conditional Type เป็นดังนี้:

T extends U ? X : Y

ตัวอย่างการใช้งานจริง

การตรวจสอบว่าไทป์เป็นสตริงหรือไม่

ลองสร้างไทป์ที่คืนค่าเป็น string ถ้าไทป์อินพุตเป็นสตริง และคืนค่าเป็น number ในกรณีอื่น:

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

การดึงไทป์ออกจาก Union

คุณสามารถใช้ conditional types เพื่อดึงไทป์เฉพาะออกจาก union type ได้ ตัวอย่างเช่น การดึงไทป์ที่ไม่ใช่ null (non-nullable):

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

ในที่นี้ ถ้า T เป็น null หรือ undefined ไทป์จะกลายเป็น never ซึ่งจะถูกกรองออกจาก union type โดยการลดรูปของ TypeScript

การอนุมานไทป์ (Inferring Types)

Conditional types ยังสามารถใช้เพื่ออนุมานไทป์โดยใช้คีย์เวิร์ด infer ซึ่งช่วยให้คุณสามารถดึงไทป์ออกจากโครงสร้างไทป์ที่ซับซ้อนมากขึ้น

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

ในตัวอย่างนี้ ReturnType จะดึงไทป์ที่ฟังก์ชันคืนค่าออกมา โดยจะตรวจสอบว่า T เป็นฟังก์ชันที่รับ arguments ใดๆ และคืนค่าเป็นไทป์ R หรือไม่ ถ้าใช่ ก็จะคืนค่าเป็น R มิฉะนั้นจะคืนค่าเป็น any

การผสมผสาน Mapped Types และ Conditional Types

พลังที่แท้จริงของ Mapped Types และ Conditional Types มาจากการนำมารวมกัน ซึ่งช่วยให้คุณสร้างการแปลงไทป์ที่มีความยืดหยุ่นและสื่อความหมายได้อย่างยอดเยี่ยม

ตัวอย่าง: Deep Readonly

กรณีการใช้งานทั่วไปคือการสร้างไทป์ที่ทำให้ properties ทั้งหมดของอ็อบเจกต์ รวมถึง properties ที่ซ้อนกันอยู่ เป็นแบบ read-only ซึ่งสามารถทำได้โดยใช้ conditional type แบบเรียกซ้ำ (recursive)

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

ในที่นี้ DeepReadonly จะใช้ modifier readonly กับ properties ทั้งหมดและ properties ที่ซ้อนอยู่ภายในแบบเรียกซ้ำ ถ้า property เป็นอ็อบเจกต์ มันจะเรียก DeepReadonly บนอ็อบเจกต์นั้นซ้ำอีกครั้ง มิฉะนั้น มันก็จะใช้ modifier readonly กับ property นั้นๆ

ตัวอย่าง: การกรอง Properties ตามไทป์

สมมติว่าคุณต้องการสร้างไทป์ที่รวมเฉพาะ properties ที่มีไทป์ที่กำหนด คุณสามารถรวม Mapped Types และ Conditional Types เพื่อให้บรรลุเป้าหมายนี้ได้

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

ในตัวอย่างนี้ FilterByType จะวนซ้ำไปตาม properties ของ T และตรวจสอบว่าไทป์ของแต่ละ property ขยาย U หรือไม่ ถ้าใช่ ก็จะรวม property นั้นไว้ในไทป์ผลลัพธ์ มิฉะนั้นจะตัดออกโดยการ map key ไปยัง never สังเกตการใช้ "as" เพื่อ re-map keys จากนั้นเราใช้ `Omit` และ `keyof StringProperties` เพื่อลบ properties ที่เป็นสตริงออกจาก interface เดิม

กรณีศึกษาขั้นสูงและรูปแบบการใช้งาน

นอกเหนือจากตัวอย่างพื้นฐาน Mapped Types และ Conditional Types ยังสามารถใช้ในสถานการณ์ที่ซับซ้อนมากขึ้นเพื่อสร้างแอปพลิเคชันที่ปรับแต่งได้สูงและปลอดภัยด้านไทป์

Distributive Conditional Types

Conditional types จะมีคุณสมบัติการกระจาย (distributive) เมื่อไทป์ที่ถูกตรวจสอบเป็น union type ซึ่งหมายความว่าเงื่อนไขจะถูกนำไปใช้กับสมาชิกแต่ละตัวของ union แยกกัน และผลลัพธ์จะถูกรวมกันเป็น union type ใหม่

type ToArray<T> = T extends any ? T[] : never;

type Result6 = ToArray<string | number>; // string[] | number[]

ในตัวอย่างนี้ ToArray ถูกนำไปใช้กับสมาชิกแต่ละตัวของ union string | number แยกกัน ส่งผลให้ได้ string[] | number[] หากเงื่อนไขไม่มีคุณสมบัติการกระจาย ผลลัพธ์ที่ได้จะเป็น (string | number)[]

การใช้ Utility Types

TypeScript มี utility types ในตัวหลายอย่างที่ใช้ประโยชน์จาก Mapped Types และ Conditional Types utility types เหล่านี้สามารถใช้เป็นส่วนประกอบพื้นฐานสำหรับการแปลงไทป์ที่ซับซ้อนยิ่งขึ้น

utility types เหล่านี้เป็นเครื่องมือที่ทรงพลังที่สามารถทำให้การจัดการไทป์ที่ซับซ้อนง่ายขึ้น ตัวอย่างเช่น คุณสามารถรวม Pick และ Partial เพื่อสร้างไทป์ที่ทำให้เฉพาะบาง properties เป็น optional:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type OptionalDescriptionProduct = Optional<Product, "description">;

ในตัวอย่างนี้ OptionalDescriptionProduct จะมี properties ทั้งหมดของ Product แต่ property description จะเป็น optional

การใช้ Template Literal Types

Template Literal Types ช่วยให้คุณสร้างไทป์ตามสตริงลิเทอรัลได้ สามารถใช้ร่วมกับ Mapped Types และ Conditional Types เพื่อสร้างการแปลงไทป์แบบไดนามิกและสื่อความหมายได้ดีขึ้น ตัวอย่างเช่น คุณสามารถสร้างไทป์ที่เติมคำนำหน้าให้กับชื่อ property ทั้งหมดด้วยสตริงที่ระบุ:

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

ในตัวอย่างนี้ PrefixedSettings จะมี properties เป็น data_apiUrl และ data_timeout

แนวทางปฏิบัติที่ดีที่สุดและข้อควรพิจารณา

บทสรุป

Mapped Types และ Conditional Types เป็นฟีเจอร์ที่ทรงพลังใน TypeScript ที่ช่วยให้คุณสามารถสร้างการแปลงไทป์ที่มีความยืดหยุ่นและสื่อความหมายได้อย่างยอดเยี่ยม การเรียนรู้แนวคิดเหล่านี้อย่างเชี่ยวชาญจะช่วยให้คุณสามารถปรับปรุงความปลอดภัยของไทป์ (type safety), ความสามารถในการบำรุงรักษา และคุณภาพโดยรวมของแอปพลิเคชัน TypeScript ของคุณ ตั้งแต่การแปลงง่ายๆ เช่น การทำให้ properties เป็น optional หรือ read-only ไปจนถึงการแปลงแบบเรียกซ้ำที่ซับซ้อนและตรรกะตามเงื่อนไข ฟีเจอร์เหล่านี้มอบเครื่องมือที่คุณต้องการเพื่อสร้างแอปพลิเคชันที่แข็งแกร่งและปรับขนาดได้ หมั่นสำรวจและทดลองใช้ฟีเจอร์เหล่านี้เพื่อปลดล็อกศักยภาพสูงสุดและกลายเป็นนักพัฒนา TypeScript ที่เชี่ยวชาญยิ่งขึ้น

ในขณะที่คุณเดินทางต่อไปในเส้นทาง TypeScript อย่าลืมใช้ประโยชน์จากแหล่งข้อมูลมากมายที่มีอยู่ รวมถึงเอกสารทางการของ TypeScript, ชุมชนออนไลน์ และโปรเจกต์โอเพนซอร์ส โอบรับพลังของ Mapped Types และ Conditional Types แล้วคุณจะพร้อมรับมือกับปัญหาที่ท้าทายที่สุดที่เกี่ยวกับไทป์