เชี่ยวชาญ TypeScript Utility Types: เครื่องมือทรงพลังสำหรับแปลง Type เพิ่มการใช้โค้ดซ้ำ และเสริมความปลอดภัยให้แอปพลิเคชันของคุณ
TypeScript Utility Types: เครื่องมือจัดการ Type ในตัว
TypeScript เป็นภาษาที่ทรงพลังซึ่งนำ static typing มาสู่ JavaScript หนึ่งในคุณสมบัติหลักคือความสามารถในการจัดการกับไทป์ (types) ซึ่งช่วยให้นักพัฒนาสามารถสร้างโค้ดที่แข็งแกร่งและบำรุงรักษาง่ายขึ้น TypeScript มีชุด utility types ในตัวที่ช่วยให้การแปลงไทป์ทั่วไปง่ายขึ้น utility types เหล่านี้เป็นเครื่องมือที่ทรงคุณค่าในการเพิ่มความปลอดภัยของไทป์ (type safety) ปรับปรุงการนำโค้ดกลับมาใช้ใหม่ (code reusability) และทำให้กระบวนการพัฒนาของคุณราบรื่นขึ้น คู่มือฉบับสมบูรณ์นี้จะสำรวจ utility types ที่สำคัญที่สุดของ TypeScript พร้อมตัวอย่างที่ใช้งานได้จริงและข้อมูลเชิงลึกที่จะช่วยให้คุณใช้งานได้อย่างเชี่ยวชาญ
TypeScript Utility Types คืออะไร?
Utility types คือตัวดำเนินการไทป์ (type operators) ที่กำหนดไว้ล่วงหน้าซึ่งทำหน้าที่แปลงไทป์ที่มีอยู่ให้เป็นไทป์ใหม่ มันถูกสร้างขึ้นในภาษา TypeScript และเป็นวิธีที่กระชับและชัดเจนในการจัดการไทป์ทั่วไป การใช้ utility types สามารถลดโค้ด boilerplate ได้อย่างมาก และทำให้การกำหนดไทป์ของคุณสื่อความหมายได้ดีขึ้นและเข้าใจง่ายขึ้น
ลองนึกภาพว่ามันเป็นเหมือนฟังก์ชันที่ทำงานกับไทป์แทนที่จะเป็นค่า (values) มันรับไทป์เป็นอินพุตและส่งคืนไทป์ที่ถูกแก้ไขเป็นเอาต์พุต สิ่งนี้ช่วยให้คุณสร้างความสัมพันธ์และการแปลงไทป์ที่ซับซ้อนได้ด้วยโค้ดเพียงเล็กน้อย
ทำไมต้องใช้ Utility Types?
มีเหตุผลที่น่าสนใจหลายประการในการนำ utility types มาใช้ในโปรเจกต์ TypeScript ของคุณ:
- เพิ่มความปลอดภัยของไทป์ (Type Safety): Utility types ช่วยให้คุณบังคับใช้ข้อจำกัดของไทป์ที่เข้มงวดขึ้น ลดโอกาสเกิดข้อผิดพลาดขณะรันไทม์ และปรับปรุงความน่าเชื่อถือโดยรวมของโค้ดของคุณ
- ปรับปรุงการนำโค้ดกลับมาใช้ใหม่ (Code Reusability): ด้วยการใช้ utility types คุณสามารถสร้างคอมโพเนนต์และฟังก์ชันที่เป็น generic ซึ่งทำงานกับไทป์ที่หลากหลายได้ ส่งเสริมการใช้โค้ดซ้ำและลดความซ้ำซ้อน
- ลด Boilerplate: Utility types เป็นวิธีที่กระชับและชัดเจนในการแปลงไทป์ทั่วไป ซึ่งช่วยลดปริมาณโค้ด boilerplate ที่คุณต้องเขียน
- เพิ่มความสามารถในการอ่าน (Readability): Utility types ทำให้การกำหนดไทป์ของคุณสื่อความหมายได้ดีขึ้นและเข้าใจง่ายขึ้น ซึ่งช่วยปรับปรุงความสามารถในการอ่านและการบำรุงรักษาโค้ดของคุณ
Utility Types ที่สำคัญใน TypeScript
เรามาสำรวจ utility types ที่ใช้บ่อยและมีประโยชน์ที่สุดใน TypeScript กัน เราจะครอบคลุมถึงวัตถุประสงค์ ไวยากรณ์ และตัวอย่างการใช้งานจริงเพื่อแสดงให้เห็นถึงการใช้งาน
1. Partial<T>
Partial<T>
utility type ทำให้ property ทั้งหมดของไทป์ T
เป็น optional (ไม่บังคับ) สิ่งนี้มีประโยชน์เมื่อคุณต้องการสร้างไทป์ใหม่ที่มี property บางส่วนหรือทั้งหมดของไทป์ที่มีอยู่ แต่คุณไม่ต้องการบังคับให้มีครบทุก property
ไวยากรณ์:
type Partial<T> = { [P in keyof T]?: T[P]; };
ตัวอย่าง:
interface User {
id: number;
name: string;
email: string;
}
type OptionalUser = Partial<User>; // property ทั้งหมดกลายเป็น optional
const partialUser: OptionalUser = {
name: "Alice", // ระบุแค่ property name เท่านั้น
};
กรณีการใช้งาน: การอัปเดตอ็อบเจกต์ด้วย property เพียงบางส่วน ตัวอย่างเช่น ลองนึกภาพฟอร์มอัปเดตโปรไฟล์ผู้ใช้ คุณไม่ต้องการบังคับให้ผู้ใช้อัปเดตทุกฟิลด์พร้อมกัน
2. Required<T>
Required<T>
utility type ทำให้ property ทั้งหมดของไทป์ T
เป็น required (บังคับ) ซึ่งตรงกันข้ามกับ Partial<T>
มีประโยชน์เมื่อคุณมีไทป์ที่มี property เป็น optional และคุณต้องการให้แน่ใจว่ามี property ทั้งหมดครบถ้วน
ไวยากรณ์:
type Required<T> = { [P in keyof T]-?: T[P]; };
ตัวอย่าง:
interface Config {
apiKey?: string;
apiUrl?: string;
}
type CompleteConfig = Required<Config>; // property ทั้งหมดกลายเป็น required
const config: CompleteConfig = {
apiKey: "your-api-key",
apiUrl: "https://example.com/api",
};
กรณีการใช้งาน: การบังคับให้การตั้งค่าทั้งหมดถูกกำหนดก่อนที่จะเริ่มแอปพลิเคชัน ซึ่งสามารถช่วยป้องกันข้อผิดพลาดขณะรันไทม์ที่เกิดจากการตั้งค่าที่ขาดหายไปหรือไม่ถูกกำหนด
3. Readonly<T>
Readonly<T>
utility type ทำให้ property ทั้งหมดของไทป์ T
เป็นแบบอ่านอย่างเดียว (readonly) ซึ่งป้องกันไม่ให้คุณแก้ไข property ของอ็อบเจกต์โดยไม่ได้ตั้งใจหลังจากที่มันถูกสร้างขึ้นแล้ว สิ่งนี้ส่งเสริม immutability (การไม่เปลี่ยนแปลงข้อมูล) และปรับปรุงความสามารถในการคาดเดาการทำงานของโค้ดของคุณ
ไวยากรณ์:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
ตัวอย่าง:
interface Product {
id: number;
name: string;
price: number;
}
type ImmutableProduct = Readonly<Product>; // property ทั้งหมดกลายเป็น readonly
const product: ImmutableProduct = {
id: 123,
name: "Example Product",
price: 25.99,
};
// product.price = 29.99; // เกิดข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'price' ได้เพราะเป็น property แบบอ่านอย่างเดียว
กรณีการใช้งาน: การสร้างโครงสร้างข้อมูลที่ไม่สามารถเปลี่ยนแปลงได้ เช่น configuration objects หรือ data transfer objects (DTOs) ที่ไม่ควรถูกแก้ไขหลังจากการสร้างขึ้น มีประโยชน์อย่างยิ่งในกระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชัน (functional programming paradigms)
4. Pick<T, K extends keyof T>
Pick<T, K extends keyof T>
utility type สร้างไทป์ใหม่โดยการเลือกชุดของ property K
จากไทป์ T
สิ่งนี้มีประโยชน์เมื่อคุณต้องการเพียงแค่ส่วนหนึ่งของ property จากไทป์ที่มีอยู่
ไวยากรณ์:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
ตัวอย่าง:
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // เลือกแค่ name และ department
const employeeInfo: EmployeeNameAndDepartment = {
name: "Bob",
department: "Engineering",
};
กรณีการใช้งาน: การสร้าง data transfer objects (DTOs) เฉพาะทางที่มีเฉพาะข้อมูลที่จำเป็นสำหรับการดำเนินการบางอย่าง ซึ่งสามารถปรับปรุงประสิทธิภาพและลดปริมาณข้อมูลที่ส่งผ่านเครือข่าย ลองนึกภาพการส่งรายละเอียดผู้ใช้ไปยังไคลเอนต์แต่ไม่รวมข้อมูลที่ละเอียดอ่อนเช่นเงินเดือน คุณสามารถใช้ Pick เพื่อส่งแค่ `id` และ `name`
5. Omit<T, K extends keyof any>
Omit<T, K extends keyof any>
utility type สร้างไทป์ใหม่โดยการละเว้นชุดของ property K
จากไทป์ T
ซึ่งตรงกันข้ามกับ Pick<T, K extends keyof T>
และมีประโยชน์เมื่อคุณต้องการยกเว้น property บางอย่างออกจากไทป์ที่มีอยู่
ไวยากรณ์:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
ตัวอย่าง:
interface Event {
id: number;
title: string;
description: string;
date: Date;
location: string;
}
type EventSummary = Omit<Event, "description" | "location">; // ละเว้น description และ location
const eventPreview: EventSummary = {
id: 1,
title: "Conference",
date: new Date(),
};
กรณีการใช้งาน: การสร้างโมเดลข้อมูลเวอร์ชันที่ง่ายขึ้นสำหรับวัตถุประสงค์เฉพาะ เช่น การแสดงสรุปของกิจกรรมโดยไม่รวมคำอธิบายและสถานที่ทั้งหมด นอกจากนี้ยังสามารถใช้เพื่อลบฟิลด์ที่ละเอียดอ่อนก่อนส่งข้อมูลไปยังไคลเอนต์
6. Exclude<T, U>
Exclude<T, U>
utility type สร้างไทป์ใหม่โดยการยกเว้นไทป์ทั้งหมดที่สามารถกำหนดให้กับ U
ได้ ออกจาก T
สิ่งนี้มีประโยชน์เมื่อคุณต้องการลบไทป์บางอย่างออกจาก union type
ไวยากรณ์:
type Exclude<T, U> = T extends U ? never : T;
ตัวอย่าง:
type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";
type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"
const fileType: DocumentFileTypes = "document";
กรณีการใช้งาน: การกรอง union type เพื่อลบไทป์เฉพาะที่ไม่เกี่ยวข้องในบริบทใดบริบทหนึ่ง ตัวอย่างเช่น คุณอาจต้องการยกเว้นประเภทไฟล์บางประเภทออกจากรายการประเภทไฟล์ที่อนุญาต
7. Extract<T, U>
Extract<T, U>
utility type สร้างไทป์ใหม่โดยการดึงไทป์ทั้งหมดที่สามารถกำหนดให้กับ U
ได้ ออกมาจาก T
ซึ่งตรงกันข้ามกับ Exclude<T, U>
และมีประโยชน์เมื่อคุณต้องการเลือกไทป์ที่เฉพาะเจาะจงจาก union type
ไวยากรณ์:
type Extract<T, U> = T extends U ? T : never;
ตัวอย่าง:
type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;
type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean
const value: NonNullablePrimitives = "hello";
กรณีการใช้งาน: การเลือกไทป์เฉพาะจาก union type ตามเกณฑ์บางอย่าง ตัวอย่างเช่น คุณอาจต้องการดึงไทป์พื้นฐาน (primitive types) ทั้งหมดออกจาก union type ที่มีทั้งไทป์พื้นฐานและ object types
8. NonNullable<T>
NonNullable<T>
utility type สร้างไทป์ใหม่โดยการยกเว้น null
และ undefined
ออกจากไทป์ T
สิ่งนี้มีประโยชน์เมื่อคุณต้องการให้แน่ใจว่าไทป์ไม่สามารถเป็น null
หรือ undefined
ได้
ไวยากรณ์:
type NonNullable<T> = T extends null | undefined ? never : T;
ตัวอย่าง:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
const message: DefinitelyString = "Hello, world!";
กรณีการใช้งาน: การบังคับให้ค่าไม่เป็น null
หรือ undefined
ก่อนที่จะดำเนินการกับมัน ซึ่งสามารถช่วยป้องกันข้อผิดพลาดขณะรันไทม์ที่เกิดจากค่า null หรือ undefined ที่ไม่คาดคิด ลองพิจารณาสถานการณ์ที่คุณต้องประมวลผลที่อยู่ของผู้ใช้ และเป็นสิ่งสำคัญที่ที่อยู่จะต้องไม่เป็น null ก่อนการดำเนินการใดๆ
9. ReturnType<T extends (...args: any) => any>
ReturnType<T extends (...args: any) => any>
utility type ดึงเอา return type (ไทป์ของค่าที่ส่งคืน) ของฟังก์ชันไทป์ T
ออกมา สิ่งนี้มีประโยชน์เมื่อคุณต้องการทราบไทป์ของค่าที่ฟังก์ชันส่งคืน
ไวยากรณ์:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
ตัวอย่าง:
function fetchData(url: string): Promise<{ data: any }> {
return fetch(url).then(response => response.json());
}
type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>
async function processData(data: FetchDataReturnType) {
// ...
}
กรณีการใช้งาน: การกำหนดไทป์ของค่าที่ส่งคืนโดยฟังก์ชัน โดยเฉพาะอย่างยิ่งเมื่อต้องจัดการกับการดำเนินการแบบอะซิงโครนัสหรือลายเซ็นฟังก์ชันที่ซับซ้อน สิ่งนี้ช่วยให้คุณมั่นใจได้ว่าคุณกำลังจัดการกับค่าที่ส่งคืนอย่างถูกต้อง
10. Parameters<T extends (...args: any) => any>
Parameters<T extends (...args: any) => any>
utility type ดึงเอาไทป์ของพารามิเตอร์ของฟังก์ชันไทป์ T
ออกมาในรูปแบบ tuple สิ่งนี้มีประโยชน์เมื่อคุณต้องการทราบไทป์ของอาร์กิวเมนต์ที่ฟังก์ชันยอมรับ
ไวยากรณ์:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
ตัวอย่าง:
function createUser(name: string, age: number, email: string): void {
// ...
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]
function logUser(...args: CreateUserParams) {
console.log("Creating user with:", args);
}
กรณีการใช้งาน: การกำหนดไทป์ของอาร์กิวเมนต์ที่ฟังก์ชันยอมรับ ซึ่งมีประโยชน์สำหรับการสร้างฟังก์ชัน generic หรือ decorators ที่ต้องทำงานกับฟังก์ชันที่มีลายเซ็นต่างกัน ช่วยให้มั่นใจในความปลอดภัยของไทป์เมื่อส่งอาร์กิวเมนต์ไปยังฟังก์ชันแบบไดนามิก
11. ConstructorParameters<T extends abstract new (...args: any) => any>
ConstructorParameters<T extends abstract new (...args: any) => any>
utility type ดึงเอาไทป์ของพารามิเตอร์ของ constructor function type T
ออกมาในรูปแบบ tuple สิ่งนี้มีประโยชน์เมื่อคุณต้องการทราบไทป์ของอาร์กิวเมนต์ที่ constructor ยอมรับ
ไวยากรณ์:
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
ตัวอย่าง:
class Logger {
constructor(public prefix: string, public enabled: boolean) {}
log(message: string) {
if (this.enabled) {
console.log(`${this.prefix}: ${message}`);
}
}
}
type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]
function createLogger(...args: LoggerConstructorParams) {
return new Logger(...args);
}
กรณีการใช้งาน: คล้ายกับ Parameters
แต่ใช้สำหรับ constructor functions โดยเฉพาะ ช่วยในการสร้าง factories หรือ dependency injection systems ที่คุณต้องสร้างอินสแตนซ์ของคลาสแบบไดนามิกด้วยลายเซ็น constructor ที่แตกต่างกัน
12. InstanceType<T extends abstract new (...args: any) => any>
InstanceType<T extends abstract new (...args: any) => any>
utility type ดึงเอา instance type ของ constructor function type T
ออกมา สิ่งนี้มีประโยชน์เมื่อคุณต้องการทราบไทป์ของอ็อบเจกต์ที่ constructor สร้างขึ้น
ไวยากรณ์:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
ตัวอย่าง:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterInstance = InstanceType<typeof Greeter>; // Greeter
const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());
กรณีการใช้งาน: การกำหนดไทป์ของอ็อบเจกต์ที่สร้างโดย constructor ซึ่งมีประโยชน์เมื่อทำงานกับการสืบทอด (inheritance) หรือ polymorphism เป็นวิธีที่ปลอดภัยต่อไทป์ในการอ้างอิงถึงอินสแตนซ์ของคลาส
13. Record<K extends keyof any, T>
Record<K extends keyof any, T>
utility type สร้าง object type ที่มี property keys เป็น K
และ property values เป็น T
สิ่งนี้มีประโยชน์สำหรับการสร้างไทป์ที่มีลักษณะคล้าย dictionary ซึ่งคุณทราบคีย์ล่วงหน้า
ไวยากรณ์:
type Record<K extends keyof any, T> = { [P in K]: T; };
ตัวอย่าง:
type CountryCode = "US" | "CA" | "GB" | "DE";
type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }
const currencies: CurrencyMap = {
US: "USD",
CA: "CAD",
GB: "GBP",
DE: "EUR",
};
กรณีการใช้งาน: การสร้างอ็อบเจกต์ที่มีลักษณะคล้าย dictionary ซึ่งคุณมีชุดของคีย์ที่แน่นอนและต้องการให้แน่ใจว่าทุกคีย์มีค่าเป็นไทป์ที่ระบุ ซึ่งเป็นเรื่องปกติเมื่อทำงานกับไฟล์การกำหนดค่า (configuration files) การแมปข้อมูล (data mappings) หรือตารางค้นหา (lookup tables)
Custom Utility Types
แม้ว่า utility types ในตัวของ TypeScript จะทรงพลัง แต่คุณก็สามารถสร้าง utility types ของคุณเองเพื่อตอบสนองความต้องการเฉพาะในโปรเจกต์ของคุณได้ สิ่งนี้ช่วยให้คุณสามารถห่อหุ้มการแปลงไทป์ที่ซับซ้อนและนำกลับมาใช้ใหม่ได้ทั่วทั้งโค้ดเบสของคุณ
ตัวอย่าง:
// utility type สำหรับดึง key ของอ็อบเจกต์ที่มีไทป์ที่ระบุ
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
interface Person {
name: string;
age: number;
address: string;
phoneNumber: number;
}
type StringKeys = KeysOfType<Person, string>; // "name" | "address"
แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ Utility Types
- ใช้ชื่อที่สื่อความหมาย: ตั้งชื่อ utility types ของคุณให้มีความหมายและบ่งบอกถึงวัตถุประสงค์อย่างชัดเจน สิ่งนี้ช่วยปรับปรุงความสามารถในการอ่านและการบำรุงรักษาโค้ดของคุณ
- จัดทำเอกสารสำหรับ utility types ของคุณ: เพิ่มคอมเมนต์เพื่ออธิบายว่า utility types ของคุณทำอะไรและควรใช้งานอย่างไร สิ่งนี้ช่วยให้นักพัฒนาคนอื่นเข้าใจโค้ดของคุณและใช้งานได้อย่างถูกต้อง
- ทำให้เรียบง่าย: หลีกเลี่ยงการสร้าง utility types ที่ซับซ้อนเกินไปและเข้าใจยาก แบ่งการแปลงที่ซับซ้อนออกเป็น utility types ที่เล็กกว่าและจัดการได้ง่ายกว่า
- ทดสอบ utility types ของคุณ: เขียน unit test เพื่อให้แน่ใจว่า utility types ของคุณทำงานได้อย่างถูกต้อง สิ่งนี้ช่วยป้องกันข้อผิดพลาดที่ไม่คาดคิดและรับประกันว่าไทป์ของคุณทำงานตามที่คาดหวัง
- พิจารณาถึงประสิทธิภาพ: แม้ว่าโดยทั่วไป utility types จะไม่มีผลกระทบต่อประสิทธิภาพอย่างมีนัยสำคัญ แต่ควรคำนึงถึงความซับซ้อนของการแปลงไทป์ของคุณ โดยเฉพาะในโปรเจกต์ขนาดใหญ่
บทสรุป
TypeScript utility types เป็นเครื่องมือที่ทรงพลังซึ่งสามารถปรับปรุงความปลอดภัยของไทป์, การนำกลับมาใช้ใหม่, และการบำรุงรักษาโค้ดของคุณได้อย่างมาก ด้วยการใช้งาน utility types เหล่านี้อย่างเชี่ยวชาญ คุณจะสามารถเขียนแอปพลิเคชัน TypeScript ที่แข็งแกร่งและสื่อความหมายได้ดีขึ้น คู่มือนี้ได้ครอบคลุม utility types ที่สำคัญที่สุดของ TypeScript พร้อมตัวอย่างที่ใช้งานได้จริงและข้อมูลเชิงลึกที่จะช่วยให้คุณนำไปใช้ในโปรเจกต์ของคุณ
อย่าลืมทดลองใช้ utility types เหล่านี้และสำรวจว่าสามารถนำไปใช้แก้ปัญหาเฉพาะในโค้ดของคุณได้อย่างไร เมื่อคุณคุ้นเคยกับมันมากขึ้น คุณจะพบว่าตัวเองใช้มันบ่อยขึ้นเรื่อยๆ เพื่อสร้างแอปพลิเคชัน TypeScript ที่สะอาดขึ้น บำรุงรักษาง่ายขึ้น และปลอดภัยต่อไทป์มากขึ้น ไม่ว่าคุณจะสร้างเว็บแอปพลิเคชัน, แอปพลิเคชันฝั่งเซิร์ฟเวอร์, หรืออะไรก็ตาม utility types มอบชุดเครื่องมืออันมีค่าสำหรับปรับปรุงกระบวนการพัฒนาและคุณภาพโค้ดของคุณ ด้วยการใช้ประโยชน์จากเครื่องมือจัดการไทป์ในตัวเหล่านี้ คุณจะสามารถปลดล็อกศักยภาพสูงสุดของ TypeScript และเขียนโค้ดที่ทั้งสื่อความหมายและแข็งแกร่งได้