ไทย

เชี่ยวชาญ 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 ของคุณ:

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

บทสรุป

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

อย่าลืมทดลองใช้ utility types เหล่านี้และสำรวจว่าสามารถนำไปใช้แก้ปัญหาเฉพาะในโค้ดของคุณได้อย่างไร เมื่อคุณคุ้นเคยกับมันมากขึ้น คุณจะพบว่าตัวเองใช้มันบ่อยขึ้นเรื่อยๆ เพื่อสร้างแอปพลิเคชัน TypeScript ที่สะอาดขึ้น บำรุงรักษาง่ายขึ้น และปลอดภัยต่อไทป์มากขึ้น ไม่ว่าคุณจะสร้างเว็บแอปพลิเคชัน, แอปพลิเคชันฝั่งเซิร์ฟเวอร์, หรืออะไรก็ตาม utility types มอบชุดเครื่องมืออันมีค่าสำหรับปรับปรุงกระบวนการพัฒนาและคุณภาพโค้ดของคุณ ด้วยการใช้ประโยชน์จากเครื่องมือจัดการไทป์ในตัวเหล่านี้ คุณจะสามารถปลดล็อกศักยภาพสูงสุดของ TypeScript และเขียนโค้ดที่ทั้งสื่อความหมายและแข็งแกร่งได้