ไทย

ปลดล็อกพลังของ TypeScript utility types เพื่อเขียนโค้ดที่สะอาดขึ้น ดูแลรักษาง่ายขึ้น และปลอดภัยด้านประเภทข้อมูลมากขึ้น สำรวจการใช้งานจริงพร้อมตัวอย่างที่นักพัฒนาทั่วโลกนำไปปรับใช้ได้

เชี่ยวชาญ TypeScript Utility Types: คู่มือเชิงปฏิบัติสำหรับนักพัฒนาทั่วโลก

TypeScript มีชุดของ utility types ที่ทรงพลังซึ่งสามารถช่วยเพิ่มความปลอดภัยของประเภทข้อมูล (type safety) ความสามารถในการอ่าน (readability) และความสามารถในการบำรุงรักษา (maintainability) ของโค้ดของคุณได้อย่างมาก utility types เหล่านี้คือการแปลงประเภทข้อมูลที่กำหนดไว้ล่วงหน้าซึ่งคุณสามารถนำไปใช้กับประเภทข้อมูลที่มีอยู่ได้ ช่วยให้คุณไม่ต้องเขียนโค้ดที่ซ้ำซ้อนและมีข้อผิดพลาดได้ง่าย คู่มือนี้จะสำรวจ utility types ต่างๆ พร้อมตัวอย่างการใช้งานจริงที่นักพัฒนาทั่วโลกสามารถนำไปปรับใช้ได้

ทำไมต้องใช้ Utility Types?

Utility types จัดการสถานการณ์การจัดการประเภทข้อมูลที่พบบ่อย โดยการใช้ประโยชน์จาก utility types คุณสามารถ:

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 และมุ่งมั่นที่จะเขียนโค้ดที่เข้าใจและบำรุงรักษาง่ายเสมอ ไม่ว่าเพื่อนนักพัฒนาของคุณจะอยู่ที่ใดก็ตาม

เชี่ยวชาญ TypeScript Utility Types: คู่มือเชิงปฏิบัติสำหรับนักพัฒนาทั่วโลก | MLOG