ไทย

สำรวจ TypeScript template literal types และวิธีนำไปใช้สร้าง API ที่มีความปลอดภัยของไทป์สูงและดูแลรักษาง่าย เพื่อปรับปรุงคุณภาพโค้ดและประสบการณ์ของนักพัฒนา

TypeScript Template Literal Types เพื่อ API ที่ปลอดภัยต่อไทป์ (Type-Safe)

TypeScript template literal types เป็นฟีเจอร์ที่ทรงพลังซึ่งเปิดตัวใน TypeScript 4.1 ที่ช่วยให้คุณสามารถจัดการสตริงในระดับไทป์ (type level) ได้ ฟีเจอร์นี้เปิดโอกาสมากมายในการสร้าง API ที่มีความปลอดภัยของไทป์ (type-safe) สูงและดูแลรักษาง่าย ช่วยให้คุณสามารถตรวจจับข้อผิดพลาดได้ตั้งแต่ตอนคอมไพล์ (compile time) ซึ่งปกติแล้วจะพบได้ก็ต่อเมื่อโปรแกรมทำงาน (runtime) เท่านั้น ซึ่งส่งผลให้ประสบการณ์ของนักพัฒนาดีขึ้น การรีแฟคเตอร์ทำได้ง่ายขึ้น และโค้ดมีความเสถียรมากขึ้น

Template Literal Types คืออะไร?

โดยแก่นแท้แล้ว template literal types คือไทป์สตริงลิเทอรัล (string literal types) ที่สามารถสร้างขึ้นได้โดยการรวมไทป์สตริงลิเทอรัล, union types, และตัวแปรไทป์ (type variables) เข้าด้วยกัน ลองนึกภาพว่ามันคือการประมาณค่าสตริง (string interpolation) สำหรับไทป์ สิ่งนี้ช่วยให้คุณสร้างไทป์ใหม่จากไทป์ที่มีอยู่เดิมได้ ทำให้มีความยืดหยุ่นและแสดงผลได้หลากหลาย

นี่คือตัวอย่างง่ายๆ:

type Greeting = "Hello, World!";

type PersonalizedGreeting<T extends string> = `Hello, ${T}!`;

type MyGreeting = PersonalizedGreeting<"Alice">; // type MyGreeting = "Hello, Alice!"

ในตัวอย่างนี้ PersonalizedGreeting คือ template literal type ที่รับพารามิเตอร์ไทป์แบบเจเนอริก (generic type parameter) T ซึ่งต้องเป็นสตริง จากนั้นจะสร้างไทป์ใหม่โดยการประมาณค่าสตริงลิเทอรัล "Hello, " ด้วยค่าของ T และสตริงลิเทอรัล "!" ไทป์ที่ได้คือ MyGreeting จะเป็น "Hello, Alice!"

ประโยชน์ของการใช้ Template Literal Types

กรณีการใช้งานจริง

1. การกำหนด API Endpoint

Template literal types สามารถใช้เพื่อกำหนดไทป์ของ API endpoint เพื่อให้แน่ใจว่ามีการส่งพารามิเตอร์ที่ถูกต้องไปยัง API และการตอบสนองถูกจัดการอย่างถูกต้อง ลองพิจารณาแพลตฟอร์มอีคอมเมิร์ซที่รองรับหลายสกุลเงิน เช่น USD, EUR และ JPY

type Currency = "USD" | "EUR" | "JPY";
type ProductID = string; //ในทางปฏิบัติ นี่อาจเป็นไทป์ที่เฉพาะเจาะจงกว่านี้

type GetProductEndpoint<C extends Currency> = `/products/${ProductID}/${C}`;

type USDEndpoint = GetProductEndpoint<"USD">; // type USDEndpoint = "/products/${string}/USD"

ตัวอย่างนี้กำหนดไทป์ GetProductEndpoint ที่รับสกุลเงินเป็นพารามิเตอร์ไทป์ ไทป์ผลลัพธ์คือไทป์สตริงลิเทอรัลที่แสดงถึง API endpoint สำหรับการดึงข้อมูลผลิตภัณฑ์ในสกุลเงินที่ระบุ การใช้วิธีนี้จะช่วยให้คุณมั่นใจได้ว่า API endpoint ถูกสร้างขึ้นอย่างถูกต้องเสมอและใช้สกุลเงินที่ถูกต้อง

2. การตรวจสอบข้อมูล

Template literal types สามารถใช้เพื่อตรวจสอบความถูกต้องของข้อมูล ณ เวลาคอมไพล์ได้ ตัวอย่างเช่น คุณสามารถใช้เพื่อตรวจสอบรูปแบบของหมายเลขโทรศัพท์หรือที่อยู่อีเมล ลองนึกภาพว่าคุณต้องการตรวจสอบหมายเลขโทรศัพท์ระหว่างประเทศซึ่งมีรูปแบบแตกต่างกันไปตามรหัสประเทศ

type CountryCode = "+1" | "+44" | "+81"; // สหรัฐอเมริกา, สหราชอาณาจักร, ญี่ปุ่น
type PhoneNumber<C extends CountryCode, N extends string> = `${C}-${N}`;

type ValidUSPhoneNumber = PhoneNumber<"+1", "555-123-4567">; // type ValidUSPhoneNumber = "+1-555-123-4567"

//หมายเหตุ: การตรวจสอบที่ซับซ้อนกว่านี้อาจต้องใช้ template literal types ร่วมกับ conditional types

ตัวอย่างนี้แสดงวิธีสร้างไทป์หมายเลขโทรศัพท์พื้นฐานที่บังคับรูปแบบเฉพาะ การตรวจสอบที่ซับซ้อนมากขึ้นอาจเกี่ยวข้องกับการใช้ conditional types และรูปแบบที่คล้ายกับนิพจน์ปรกติ (regular expression) ภายใน template literal

3. การสร้างโค้ด

Template literal types สามารถใช้เพื่อสร้างโค้ด ณ เวลาคอมไพล์ได้ ตัวอย่างเช่น คุณสามารถใช้เพื่อสร้างชื่อคอมโพเนนต์ React ตามชื่อของข้อมูลที่แสดงผล รูปแบบทั่วไปคือการสร้างชื่อคอมโพเนนต์ตามรูปแบบ <Entity>Details

type Entity = "User" | "Product" | "Order";
type ComponentName<E extends Entity> = `${E}Details`;

type UserDetailsComponent = ComponentName<"User">; // type UserDetailsComponent = "UserDetails"

สิ่งนี้ช่วยให้คุณสร้างชื่อคอมโพเนนต์โดยอัตโนมัติซึ่งมีความสอดคล้องและสื่อความหมายได้ดี ลดความเสี่ยงของชื่อที่ขัดแย้งกันและปรับปรุงความสามารถในการอ่านโค้ด

4. การจัดการอีเวนต์ (Event Handling)

Template literal types เหมาะอย่างยิ่งสำหรับการกำหนดชื่ออีเวนต์ (event) ในลักษณะที่ปลอดภัยต่อไทป์ ทำให้มั่นใจได้ว่า event listeners ถูกลงทะเบียนอย่างถูกต้องและ event handlers ได้รับข้อมูลที่คาดหวัง ลองพิจารณาระบบที่อีเวนต์ถูกจัดหมวดหมู่ตามโมดูลและประเภทอีเวนต์ โดยคั่นด้วยเครื่องหมายโคลอน

type Module = "user" | "product" | "order";
type EventType = "created" | "updated" | "deleted";
type EventName<M extends Module, E extends EventType> = `${M}:${E}`;

type UserCreatedEvent = EventName<"user", "created">; // type UserCreatedEvent = "user:created"

interface EventMap {
  [key: EventName<Module, EventType>]: (data: any) => void; //ตัวอย่าง: ไทป์สำหรับการจัดการอีเวนต์
}

ตัวอย่างนี้สาธิตวิธีการสร้างชื่ออีเวนต์ที่สอดคล้องกับรูปแบบที่สม่ำเสมอ ซึ่งช่วยปรับปรุงโครงสร้างโดยรวมและความปลอดภัยของไทป์ในระบบอีเวนต์

เทคนิคขั้นสูง

1. การใช้ร่วมกับ Conditional Types

Template literal types สามารถใช้ร่วมกับ conditional types เพื่อสร้างการแปลงไทป์ที่ซับซ้อนยิ่งขึ้นได้ Conditional types ช่วยให้คุณกำหนดไทป์ที่ขึ้นอยู่กับไทป์อื่น ๆ ทำให้สามารถดำเนินการตรรกะที่ซับซ้อนในระดับไทป์ได้

type ToUpperCase<S extends string> = S extends Uppercase<S> ? S : Uppercase<S>;

type MaybeUpperCase<S extends string, Upper extends boolean> = Upper extends true ? ToUpperCase<S> : S;

type Example = MaybeUpperCase<"hello", true>; // type Example = "HELLO"
type Example2 = MaybeUpperCase<"world", false>; // type Example2 = "world"

ในตัวอย่างนี้ MaybeUpperCase รับสตริงและบูลีน หากบูลีนเป็น true มันจะแปลงสตริงเป็นตัวพิมพ์ใหญ่ มิฉะนั้นจะคืนค่าสตริงตามเดิม สิ่งนี้แสดงให้เห็นว่าคุณสามารถแก้ไขไทป์สตริงตามเงื่อนไขได้อย่างไร

2. การใช้กับ Mapped Types

Template literal types สามารถใช้กับ mapped types เพื่อแปลงคีย์ของไทป์อ็อบเจกต์ได้ Mapped types ช่วยให้คุณสร้างไทป์ใหม่โดยการวนซ้ำคีย์ของไทป์ที่มีอยู่และใช้การแปลงกับแต่ละคีย์ กรณีการใช้งานทั่วไปคือการเพิ่มคำนำหน้า (prefix) หรือคำต่อท้าย (suffix) ให้กับคีย์ของอ็อบเจกต์

type MyObject = {
  name: string;
  age: number;
};

type AddPrefix<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${string & K}`]: T[K];
};

type PrefixedObject = AddPrefix<MyObject, "data_">;
// type PrefixedObject = {
//    data_name: string;
//    data_age: number;
// }

ในที่นี้ AddPrefix รับไทป์อ็อบเจกต์และคำนำหน้า จากนั้นจะสร้างไทป์อ็อบเจกต์ใหม่ที่มีคุณสมบัติเหมือนเดิม แต่มีการเพิ่มคำนำหน้าเข้าไปในแต่ละคีย์ สิ่งนี้มีประโยชน์สำหรับการสร้าง data transfer objects (DTOs) หรือไทป์อื่น ๆ ที่คุณต้องการแก้ไขชื่อของคุณสมบัติ

3. ไทป์สำหรับการจัดการสตริงในตัว (Intrinsic String Manipulation Types)

TypeScript มีไทป์สำหรับการจัดการสตริงในตัวหลายตัว เช่น Uppercase, Lowercase, Capitalize และ Uncapitalize ซึ่งสามารถใช้ร่วมกับ template literal types เพื่อทำการแปลงสตริงที่ซับซ้อนมากขึ้น

type MyString = "hello world";

type CapitalizedString = Capitalize<MyString>; // type CapitalizedString = "Hello world"

type UpperCasedString = Uppercase<MyString>;   // type UpperCasedString = "HELLO WORLD"

ไทป์ในตัวเหล่านี้ทำให้การจัดการสตริงทั่วไปทำได้ง่ายขึ้นโดยไม่ต้องเขียนตรรกะไทป์ที่กำหนดเอง

แนวทางปฏิบัติที่ดีที่สุด (Best Practices)

ข้อควรระวังทั่วไป (Common Pitfalls)

ทางเลือกอื่นๆ

ในขณะที่ template literal types เป็นวิธีที่มีประสิทธิภาพในการทำให้การพัฒนา API มีความปลอดภัยของไทป์ แต่ก็มีแนวทางอื่นที่อาจเหมาะสมกว่าในบางสถานการณ์

บทสรุป

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

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