คู่มือฉบับสมบูรณ์เกี่ยวกับ 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;
};
T
: ไทป์อินพุตที่คุณต้องการนำมา mapK in keyof T
: วนซ้ำไปตาม key แต่ละตัวในไทป์อินพุตT
keyof T
จะสร้าง union ของชื่อ property ทั้งหมดในT
และK
จะแทน key แต่ละตัวในระหว่างการวนซ้ำTransformation
: การแปลงที่คุณต้องการนำไปใช้กับแต่ละ property ซึ่งอาจเป็นการเพิ่ม modifier (เช่นreadonly
หรือ?
) การเปลี่ยนไทป์ หรืออื่นๆ
ตัวอย่างการใช้งานจริง
การทำให้ 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
T
: ไทป์ที่กำลังถูกตรวจสอบU
: ไทป์ที่T
กำลังขยาย (เงื่อนไข)X
: ไทป์ที่จะคืนค่าถ้าT
ขยายU
(เงื่อนไขเป็นจริง)Y
: ไทป์ที่จะคืนค่าถ้าT
ไม่ได้ขยายU
(เงื่อนไขเป็นเท็จ)
ตัวอย่างการใช้งานจริง
การตรวจสอบว่าไทป์เป็นสตริงหรือไม่
ลองสร้างไทป์ที่คืนค่าเป็น 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 เหล่านี้สามารถใช้เป็นส่วนประกอบพื้นฐานสำหรับการแปลงไทป์ที่ซับซ้อนยิ่งขึ้น
Partial<T>
: ทำให้ properties ทั้งหมดของT
เป็น optionalRequired<T>
: ทำให้ properties ทั้งหมดของT
เป็น requiredReadonly<T>
: ทำให้ properties ทั้งหมดของT
เป็น read-onlyPick<T, K>
: เลือกชุดของ propertiesK
จากT
Omit<T, K>
: ลบชุดของ propertiesK
ออกจากT
Record<K, T>
: สร้างไทป์ที่มีชุดของ propertiesK
ซึ่งมีไทป์เป็นT
Exclude<T, U>
: ตัดไทป์ทั้งหมดที่สามารถกำหนดให้กับU
ได้ออกจากT
Extract<T, U>
: ดึงไทป์ทั้งหมดที่สามารถกำหนดให้กับU
ได้ออกจากT
NonNullable<T>
: ตัดnull
และundefined
ออกจากT
Parameters<T>
: ดึงพารามิเตอร์ของฟังก์ชันไทป์T
ReturnType<T>
: ดึงไทป์ที่คืนค่าของฟังก์ชันไทป์T
InstanceType<T>
: ดึง instance type ของ constructor function typeT
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 จะทรงพลัง แต่ก็สามารถทำให้โค้ดของคุณซับซ้อนขึ้นได้ พยายามทำให้การแปลงไทป์ของคุณเรียบง่ายที่สุดเท่าที่จะทำได้
- ใช้ Utility Types: ใช้ประโยชน์จาก utility types ที่มีมาในตัวของ TypeScript ทุกครั้งที่เป็นไปได้ สิ่งเหล่านี้ผ่านการทดสอบมาอย่างดีและสามารถทำให้โค้ดของคุณง่ายขึ้น
- จัดทำเอกสารสำหรับไทป์ของคุณ: จัดทำเอกสารอธิบายการแปลงไทป์ของคุณอย่างชัดเจน โดยเฉพาะอย่างยิ่งหากมีความซับซ้อน ซึ่งจะช่วยให้นักพัฒนาคนอื่นๆ เข้าใจโค้ดของคุณได้
- ทดสอบไทป์ของคุณ: ใช้การตรวจสอบไทป์ของ TypeScript เพื่อให้แน่ใจว่าการแปลงไทป์ของคุณทำงานตามที่คาดไว้ คุณสามารถเขียน unit test เพื่อตรวจสอบการทำงานของไทป์ของคุณได้
- คำนึงถึงประสิทธิภาพ: การแปลงไทป์ที่ซับซ้อนอาจส่งผลต่อประสิทธิภาพของคอมไพเลอร์ TypeScript ของคุณ โปรดคำนึงถึงความซับซ้อนของไทป์และหลีกเลี่ยงการคำนวณที่ไม่จำเป็น
บทสรุป
Mapped Types และ Conditional Types เป็นฟีเจอร์ที่ทรงพลังใน TypeScript ที่ช่วยให้คุณสามารถสร้างการแปลงไทป์ที่มีความยืดหยุ่นและสื่อความหมายได้อย่างยอดเยี่ยม การเรียนรู้แนวคิดเหล่านี้อย่างเชี่ยวชาญจะช่วยให้คุณสามารถปรับปรุงความปลอดภัยของไทป์ (type safety), ความสามารถในการบำรุงรักษา และคุณภาพโดยรวมของแอปพลิเคชัน TypeScript ของคุณ ตั้งแต่การแปลงง่ายๆ เช่น การทำให้ properties เป็น optional หรือ read-only ไปจนถึงการแปลงแบบเรียกซ้ำที่ซับซ้อนและตรรกะตามเงื่อนไข ฟีเจอร์เหล่านี้มอบเครื่องมือที่คุณต้องการเพื่อสร้างแอปพลิเคชันที่แข็งแกร่งและปรับขนาดได้ หมั่นสำรวจและทดลองใช้ฟีเจอร์เหล่านี้เพื่อปลดล็อกศักยภาพสูงสุดและกลายเป็นนักพัฒนา TypeScript ที่เชี่ยวชาญยิ่งขึ้น
ในขณะที่คุณเดินทางต่อไปในเส้นทาง TypeScript อย่าลืมใช้ประโยชน์จากแหล่งข้อมูลมากมายที่มีอยู่ รวมถึงเอกสารทางการของ TypeScript, ชุมชนออนไลน์ และโปรเจกต์โอเพนซอร์ส โอบรับพลังของ Mapped Types และ Conditional Types แล้วคุณจะพร้อมรับมือกับปัญหาที่ท้าทายที่สุดที่เกี่ยวกับไทป์