ปลดล็อกพลังของ TypeScript utility types เพื่อเขียนโค้ดที่สะอาดขึ้น ดูแลรักษาง่ายขึ้น และปลอดภัยด้านประเภทข้อมูลมากขึ้น สำรวจการใช้งานจริงพร้อมตัวอย่างที่นักพัฒนาทั่วโลกนำไปปรับใช้ได้
เชี่ยวชาญ TypeScript Utility Types: คู่มือเชิงปฏิบัติสำหรับนักพัฒนาทั่วโลก
TypeScript มีชุดของ utility types ที่ทรงพลังซึ่งสามารถช่วยเพิ่มความปลอดภัยของประเภทข้อมูล (type safety) ความสามารถในการอ่าน (readability) และความสามารถในการบำรุงรักษา (maintainability) ของโค้ดของคุณได้อย่างมาก utility types เหล่านี้คือการแปลงประเภทข้อมูลที่กำหนดไว้ล่วงหน้าซึ่งคุณสามารถนำไปใช้กับประเภทข้อมูลที่มีอยู่ได้ ช่วยให้คุณไม่ต้องเขียนโค้ดที่ซ้ำซ้อนและมีข้อผิดพลาดได้ง่าย คู่มือนี้จะสำรวจ utility types ต่างๆ พร้อมตัวอย่างการใช้งานจริงที่นักพัฒนาทั่วโลกสามารถนำไปปรับใช้ได้
ทำไมต้องใช้ Utility Types?
Utility types จัดการสถานการณ์การจัดการประเภทข้อมูลที่พบบ่อย โดยการใช้ประโยชน์จาก utility types คุณสามารถ:
- ลดโค้ดที่ซ้ำซ้อน (boilerplate code): หลีกเลี่ยงการเขียนคำจำกัดความประเภทข้อมูลที่ซ้ำซาก
- ปรับปรุงความปลอดภัยของประเภทข้อมูล (type safety): ตรวจสอบให้แน่ใจว่าโค้ดของคุณเป็นไปตามข้อจำกัดของประเภทข้อมูล
- เพิ่มความสามารถในการอ่านโค้ด: ทำให้คำจำกัดความประเภทข้อมูลของคุณกระชับและเข้าใจง่ายขึ้น
- เพิ่มความสามารถในการบำรุงรักษา: ทำให้การแก้ไขง่ายขึ้นและลดความเสี่ยงในการเกิดข้อผิดพลาด
Core Utility Types
Partial<T>
Partial<T>
สร้างประเภทข้อมูลที่ทุกคุณสมบัติของ T
ถูกตั้งค่าเป็นทางเลือก (optional) สิ่งนี้มีประโยชน์อย่างยิ่งเมื่อคุณต้องการสร้างประเภทข้อมูลสำหรับการอัปเดตบางส่วนหรือวัตถุการตั้งค่า (configuration objects)
ตัวอย่าง:
สมมติว่าคุณกำลังสร้างแพลตฟอร์มอีคอมเมิร์ซที่มีลูกค้าจากหลากหลายภูมิภาค คุณมีประเภทข้อมูล Customer
:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber: string;
address: {
street: string;
city: string;
country: string;
postalCode: string;
};
preferences?: {
language: string;
currency: string;
}
}
เมื่ออัปเดตข้อมูลลูกค้า คุณอาจไม่ต้องการให้ระบุข้อมูลทุกฟิลด์ Partial<Customer>
ช่วยให้คุณสามารถกำหนดประเภทข้อมูลที่ทุกคุณสมบัติของ Customer
เป็นทางเลือกได้:
type PartialCustomer = Partial<Customer>;
function updateCustomer(id: string, updates: PartialCustomer): void {
// ... การใช้งานเพื่ออัปเดตลูกค้าด้วย ID ที่กำหนด
}
updateCustomer("123", { firstName: "John", lastName: "Doe" }); // Valid
updateCustomer("456", { address: { city: "London" } }); // Valid
Readonly<T>
Readonly<T>
สร้างประเภทข้อมูลที่ทุกคุณสมบัติของ T
ถูกตั้งค่าเป็น readonly
ซึ่งป้องกันการแก้ไขหลังจากเริ่มต้น (initialization) สิ่งนี้มีคุณค่าในการรับรองความไม่เปลี่ยนแปลง (immutability)
ตัวอย่าง:
พิจารณาวัตถุการตั้งค่า (configuration object) สำหรับแอปพลิเคชันทั่วโลกของคุณ:
interface AppConfig {
apiUrl: string;
theme: string;
supportedLanguages: string[];
version: string; // เพิ่มเวอร์ชัน
}
const config: AppConfig = {
apiUrl: "https://api.example.com",
theme: "dark",
supportedLanguages: ["en", "fr", "de", "es", "zh"],
version: "1.0.0"
};
เพื่อป้องกันการแก้ไขการตั้งค่าโดยไม่ตั้งใจหลังจากการเริ่มต้น คุณสามารถใช้ Readonly<AppConfig>
ได้:
type ReadonlyAppConfig = Readonly<AppConfig>;
const readonlyConfig: ReadonlyAppConfig = {
apiUrl: "https://api.example.com",
theme: "dark",
supportedLanguages: ["en", "fr", "de", "es", "zh"],
version: "1.0.0"
};
// readonlyConfig.apiUrl = "https://newapi.example.com"; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'apiUrl' ได้เนื่องจากเป็นคุณสมบัติแบบอ่านอย่างเดียว (read-only property).
Pick<T, K>
Pick<T, K>
สร้างประเภทข้อมูลโดยการเลือกชุดคุณสมบัติ K
จาก T
โดยที่ K
คือการรวมกันของ string literal types ที่แสดงถึงชื่อคุณสมบัติที่คุณต้องการรวม
ตัวอย่าง:
สมมติว่าคุณมี interface Event
ที่มีคุณสมบัติต่างๆ:
interface Event {
id: string;
title: string;
description: string;
location: string;
startTime: Date;
endTime: Date;
organizer: string;
attendees: string[];
}
หากคุณต้องการเพียงแค่ title
, location
และ startTime
สำหรับคอมโพเนนต์การแสดงผลเฉพาะ คุณสามารถใช้ Pick
ได้:
type EventSummary = Pick<Event, "title" | "location" | "startTime">;
function displayEventSummary(event: EventSummary): void {
console.log(`Event: ${event.title} at ${event.location} on ${event.startTime}`);
}
Omit<T, K>
Omit<T, K>
สร้างประเภทข้อมูลโดยการยกเว้นชุดคุณสมบัติ K
จาก T
โดยที่ K
คือการรวมกันของ string literal types ที่แสดงถึงชื่อคุณสมบัติที่คุณต้องการยกเว้น นี่คือสิ่งที่ตรงกันข้ามกับ Pick
ตัวอย่าง:
เมื่อใช้ interface Event
เดียวกัน หากคุณต้องการสร้างประเภทข้อมูลสำหรับการสร้างอีเวนต์ใหม่ คุณอาจต้องการยกเว้นคุณสมบัติ id
ซึ่งโดยทั่วไปจะถูกสร้างโดยฝั่ง backend:
type NewEvent = Omit<Event, "id">;
function createEvent(event: NewEvent): void {
// ... การใช้งานเพื่อสร้างอีเวนต์ใหม่
}
Record<K, T>
Record<K, T>
สร้างประเภทวัตถุที่มีคีย์คุณสมบัติเป็น K
และค่าคุณสมบัติเป็น T
โดยที่ K
สามารถเป็น string literal types, number literal types หรือ symbol ก็ได้ เหมาะอย่างยิ่งสำหรับการสร้าง dictionaries หรือ maps
ตัวอย่าง:
สมมติว่าคุณต้องการจัดเก็บคำแปลสำหรับส่วนต่อประสานผู้ใช้ (user interface) ของแอปพลิเคชันของคุณ คุณสามารถใช้ Record
เพื่อกำหนดประเภทข้อมูลสำหรับคำแปลของคุณได้:
type Translations = Record<string, string>;
const enTranslations: Translations = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our platform!"
};
const frTranslations: Translations = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre plateforme !"
};
function translate(key: string, language: string): string {
const translations = language === "en" ? enTranslations : frTranslations; //ทำให้ง่ายขึ้น
return translations[key] || key; // กลับไปใช้คีย์เดิมหากไม่พบคำแปล
}
console.log(translate("hello", "en")); // ผลลัพธ์: Hello
console.log(translate("hello", "fr")); // ผลลัพธ์: Bonjour
console.log(translate("nonexistent", "en")); // ผลลัพธ์: nonexistent
Exclude<T, U>
Exclude<T, U>
สร้างประเภทข้อมูลโดยการยกเว้นสมาชิก union ทั้งหมดใน T
ที่สามารถกำหนดค่าให้กับ U
ได้ มีประโยชน์สำหรับการกรองประเภทข้อมูลเฉพาะออกจาก union
ตัวอย่าง:
คุณอาจมีประเภทข้อมูลที่แสดงถึงประเภทอีเวนต์ต่างๆ:
type EventType = "concert" | "conference" | "workshop" | "webinar";
หากคุณต้องการสร้างประเภทข้อมูลที่ยกเว้นอีเวนต์ "webinar" คุณสามารถใช้ Exclude
ได้:
type PhysicalEvent = Exclude<EventType, "webinar">;
// PhysicalEvent ตอนนี้คือ "concert" | "conference" | "workshop"
function attendPhysicalEvent(event: PhysicalEvent): void {
console.log(`Attending a ${event}`);
}
// attendPhysicalEvent("webinar"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"webinar"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท '"concert" | "conference" | "workshop"' ได้
attendPhysicalEvent("concert"); // Valid
Extract<T, U>
Extract<T, U>
สร้างประเภทข้อมูลโดยการดึงสมาชิก union ทั้งหมดจาก T
ที่สามารถกำหนดค่าให้กับ U
ได้ นี่คือสิ่งที่ตรงกันข้ามกับ Exclude
ตัวอย่าง:
เมื่อใช้ EventType
แบบเดียวกัน คุณสามารถดึงประเภทอีเวนต์ webinar ออกมาได้:
type OnlineEvent = Extract<EventType, "webinar">;
// OnlineEvent ตอนนี้คือ "webinar"
function attendOnlineEvent(event: OnlineEvent): void {
console.log(`Attending a ${event} online`);
}
attendOnlineEvent("webinar"); // Valid
// attendOnlineEvent("concert"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"concert"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท '"webinar"' ได้
NonNullable<T>
NonNullable<T>
สร้างประเภทข้อมูลโดยการยกเว้น null
และ undefined
จาก T
ตัวอย่าง:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// DefinitelyString ตอนนี้คือ string
function processString(str: DefinitelyString): void {
console.log(str.toUpperCase());
}
// processString(null); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท 'null' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'string' ได้
// processString(undefined); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท 'undefined' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'string' ได้
processString("hello"); // Valid
ReturnType<T>
ReturnType<T>
สร้างประเภทข้อมูลที่ประกอบด้วย return type ของฟังก์ชัน T
ตัวอย่าง:
function greet(name: string): string {
return `Hello, ${name}!`;
}
type Greeting = ReturnType<typeof greet>;
// Greeting ตอนนี้คือ string
const message: Greeting = greet("World");
console.log(message);
Parameters<T>
Parameters<T>
สร้าง tuple type จากประเภทของพารามิเตอร์ของ function type T
ตัวอย่าง:
function logEvent(eventName: string, eventData: object): void {
console.log(`Event: ${eventName}`, eventData);
}
type LogEventParams = Parameters<typeof logEvent>;
// LogEventParams ตอนนี้คือ [eventName: string, eventData: object]
const params: LogEventParams = ["user_login", { userId: "123", timestamp: Date.now() }];
logEvent(...params);
ConstructorParameters<T>
ConstructorParameters<T>
สร้าง tuple หรือ array type จากประเภทของพารามิเตอร์ของ constructor function type T
โดยจะอนุมาน (infer) ประเภทของอาร์กิวเมนต์ที่ต้องส่งผ่านไปยัง constructor ของคลาส
ตัวอย่าง:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterParams = ConstructorParameters<typeof Greeter>;
// GreeterParams ตอนนี้คือ [message: string]
const paramsGreeter: GreeterParams = ["World"];
const greeterInstance = new Greeter(...paramsGreeter);
console.log(greeterInstance.greet()); // ผลลัพธ์: Hello, World
Required<T>
Required<T>
สร้างประเภทข้อมูลที่ประกอบด้วยคุณสมบัติทั้งหมดของ T
ที่ถูกตั้งค่าเป็นต้องระบุ (required) โดยจะทำให้คุณสมบัติที่เป็นทางเลือกทั้งหมดกลายเป็นคุณสมบัติที่ต้องระบุ
ตัวอย่าง:
interface UserProfile {
name: string;
age?: number;
email?: string;
}
type RequiredUserProfile = Required<UserProfile>;
// RequiredUserProfile ตอนนี้คือ { name: string; age: number; email: string; }
const completeProfile: RequiredUserProfile = {
name: "Alice",
age: 30,
email: "alice@example.com"
};
// const incompleteProfile: RequiredUserProfile = { name: "Bob" }; // ข้อผิดพลาด: คุณสมบัติ 'age' หายไปในประเภท '{ name: string; }' แต่จำเป็นต้องมีในประเภท 'Required'.
Advanced Utility Types
Template Literal Types
Template literal types ช่วยให้คุณสามารถสร้าง string literal types ใหม่ได้โดยการเชื่อม string literal types, number literal types และอื่นๆ ที่มีอยู่เข้าด้วยกัน สิ่งนี้ช่วยให้สามารถจัดการประเภทข้อมูลที่อิงตามสตริงได้อย่างทรงพลัง
ตัวอย่าง:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/users` | `/api/products`;
type RequestURL = `${HTTPMethod} ${APIEndpoint}`;
// RequestURL ตอนนี้คือ "GET /api/users" | "POST /api/users" | "PUT /api/users" | "DELETE /api/users" | "GET /api/products" | "POST /api/products" | "PUT /api/products" | "DELETE /api/products"
function makeRequest(url: RequestURL): void {
console.log(`Making request to ${url}`);
}
makeRequest("GET /api/users"); // Valid
// makeRequest("INVALID /api/users"); // ข้อผิดพลาด
Conditional Types
Conditional types ช่วยให้คุณสามารถกำหนดประเภทข้อมูลที่ขึ้นอยู่กับเงื่อนไขที่แสดงเป็นความสัมพันธ์ของประเภทข้อมูล โดยใช้คีย์เวิร์ด infer
เพื่อดึงข้อมูลประเภทข้อมูล
ตัวอย่าง:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// หาก T เป็น Promise ประเภทข้อมูลคือ U; มิฉะนั้น ประเภทข้อมูลคือ T.
async function fetchData(): Promise<number> {
return 42;
}
type Data = UnwrapPromise<ReturnType<typeof fetchData>>;
// Data ตอนนี้คือ number
function processData(data: Data): void {
console.log(data * 2);
}
processData(await fetchData());
การใช้งานจริงและสถานการณ์ในโลกแห่งความเป็นจริง
มาสำรวจสถานการณ์ในโลกแห่งความเป็นจริงที่ซับซ้อนมากขึ้น ซึ่ง utility types แสดงศักยภาพอย่างโดดเด่น
1. การจัดการฟอร์ม (Form Handling)
เมื่อจัดการกับฟอร์ม คุณมักจะพบสถานการณ์ที่คุณต้องแสดงค่าเริ่มต้นของฟอร์ม ค่าที่อัปเดตของฟอร์ม และค่าที่ส่งในขั้นสุดท้าย Utility types สามารถช่วยคุณจัดการสถานะที่แตกต่างกันเหล่านี้ได้อย่างมีประสิทธิภาพ
interface FormData {
firstName: string;
lastName: string;
email: string;
country: string; // ต้องระบุ
city?: string; // ทางเลือก
postalCode?: string;
newsletterSubscription?: boolean;
}
// ค่าฟอร์มเริ่มต้น (ฟิลด์ทางเลือก)
type InitialFormValues = Partial<FormData>;
// ค่าฟอร์มที่อัปเดตแล้ว (บางฟิลด์อาจขาดหายไป)
type UpdatedFormValues = Partial<FormData>;
// ฟิลด์ที่จำเป็นสำหรับการส่ง
type RequiredForSubmission = Required<Pick<FormData, 'firstName' | 'lastName' | 'email' | 'country'>>;
// ใช้ประเภทเหล่านี้ในคอมโพเนนต์ฟอร์มของคุณ
function initializeForm(initialValues: InitialFormValues): void { }
function updateForm(updates: UpdatedFormValues): void {}
function submitForm(data: RequiredForSubmission): void {}
const initialForm: InitialFormValues = { newsletterSubscription: true };
const updateFormValues: UpdatedFormValues = {
firstName: "John",
lastName: "Doe"
};
// const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test" }; // ข้อผิดพลาด: ขาด 'country'
const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test", country: "USA" }; //ตกลง
2. การแปลงข้อมูล API (API Data Transformation)
เมื่อดึงข้อมูลจาก API คุณอาจต้องแปลงข้อมูลให้อยู่ในรูปแบบที่แตกต่างกันสำหรับแอปพลิเคชันของคุณ Utility types สามารถช่วยคุณกำหนดโครงสร้างของข้อมูลที่ถูกแปลงได้
interface APIResponse {
user_id: string;
first_name: string;
last_name: string;
email_address: string;
profile_picture_url: string;
is_active: boolean;
}
// แปลงการตอบสนอง API ให้อยู่ในรูปแบบที่อ่านง่ายขึ้น
type UserData = {
id: string;
fullName: string;
email: string;
avatar: string;
active: boolean;
};
function transformApiResponse(response: APIResponse): UserData {
return {
id: response.user_id,
fullName: `${response.first_name} ${response.last_name}`,
email: response.email_address,
avatar: response.profile_picture_url,
active: response.is_active
};
}
function fetchAndTransformData(url: string): Promise<UserData> {
return fetch(url)
.then(response => response.json())
.then(data => transformApiResponse(data));
}
// คุณสามารถบังคับใช้ประเภทได้โดย:
function saferTransformApiResponse(response: APIResponse): UserData {
const {user_id, first_name, last_name, email_address, profile_picture_url, is_active} = response;
const transformed: UserData = {
id: user_id,
fullName: `${first_name} ${last_name}`,
email: email_address,
avatar: profile_picture_url,
active: is_active
};
return transformed;
}
3. การจัดการวัตถุการตั้งค่า (Configuration Objects)
วัตถุการตั้งค่า (Configuration objects) เป็นเรื่องปกติในหลายแอปพลิเคชัน Utility types สามารถช่วยคุณกำหนดโครงสร้างของวัตถุการตั้งค่าและรับประกันว่ามีการใช้งานอย่างถูกต้อง
interface AppSettings {
theme: "light" | "dark";
language: string;
notificationsEnabled: boolean;
apiUrl?: string; // URL API ทางเลือกสำหรับสภาพแวดล้อมที่แตกต่างกัน
timeout?: number; //ทางเลือก
}
// การตั้งค่าเริ่มต้น
const defaultSettings: AppSettings = {
theme: "light",
language: "en",
notificationsEnabled: true
};
// ฟังก์ชันสำหรับรวมการตั้งค่าผู้ใช้เข้ากับการตั้งค่าเริ่มต้น
function mergeSettings(userSettings: Partial<AppSettings>): AppSettings {
return { ...defaultSettings, ...userSettings };
}
// ใช้การตั้งค่าที่รวมกันในแอปพลิเคชันของคุณ
const mergedSettings = mergeSettings({ theme: "dark", apiUrl: "https://customapi.example.com" });
console.log(mergedSettings);
เคล็ดลับสำหรับการใช้ Utility Types อย่างมีประสิทธิภาพ
- เริ่มต้นแบบง่ายๆ: เริ่มต้นด้วย utility types พื้นฐาน เช่น
Partial
และReadonly
ก่อนที่จะก้าวไปสู่ประเภทที่ซับซ้อนมากขึ้น - ใช้ชื่อที่สื่อความหมาย: ตั้งชื่อ type aliases ของคุณให้มีความหมายเพื่อปรับปรุงความสามารถในการอ่าน
- รวม utility types: คุณสามารถรวม utility types หลายๆ ประเภทเข้าด้วยกันเพื่อให้ได้การแปลงประเภทข้อมูลที่ซับซ้อน
- ใช้ประโยชน์จากการสนับสนุนของ Editor: ใช้ประโยชน์จากการสนับสนุน Editor ที่ยอดเยี่ยมของ TypeScript เพื่อสำรวจผลกระทบของ utility types
- ทำความเข้าใจแนวคิดพื้นฐาน: ความเข้าใจอย่างถ่องแท้ในระบบประเภทข้อมูลของ TypeScript เป็นสิ่งสำคัญสำหรับการใช้ utility types อย่างมีประสิทธิภาพ
บทสรุป
TypeScript utility types เป็นเครื่องมือที่ทรงพลังที่สามารถปรับปรุงคุณภาพและความสามารถในการบำรุงรักษาโค้ดของคุณได้อย่างมาก ด้วยการทำความเข้าใจและนำ utility types เหล่านี้ไปใช้อย่างมีประสิทธิภาพ คุณจะสามารถเขียนแอปพลิเคชันที่สะอาดขึ้น ปลอดภัยด้านประเภทข้อมูลมากขึ้น และแข็งแกร่งมากขึ้น ซึ่งตอบสนองความต้องการของภูมิทัศน์การพัฒนาทั่วโลก คู่มือนี้ได้ให้ภาพรวมที่ครอบคลุมของ utility types ทั่วไปและตัวอย่างการใช้งานจริง ทดลองใช้และสำรวจศักยภาพของมันเพื่อปรับปรุงโปรเจกต์ TypeScript ของคุณ โปรดจำไว้ว่าให้ความสำคัญกับความสามารถในการอ่านและความชัดเจนเมื่อใช้ utility types และมุ่งมั่นที่จะเขียนโค้ดที่เข้าใจและบำรุงรักษาง่ายเสมอ ไม่ว่าเพื่อนนักพัฒนาของคุณจะอยู่ที่ใดก็ตาม