ไทย

สำรวจ TypeScript literal types ฟีเจอร์ทรงพลังสำหรับบังคับข้อจำกัดค่าที่เข้มงวด เพิ่มความชัดเจนของโค้ด และป้องกันข้อผิดพลาด เรียนรู้พร้อมตัวอย่างและเทคนิคขั้นสูง

TypeScript Literal Types: การควบคุมข้อจำกัดของค่าที่แน่นอนอย่างเชี่ยวชาญ

TypeScript ซึ่งเป็นส่วนขยายของ JavaScript (superset) ได้นำ static typing มาสู่โลกของการพัฒนาเว็บแบบไดนามิก หนึ่งในฟีเจอร์ที่ทรงพลังที่สุดคือแนวคิดของ literal types ซึ่งช่วยให้คุณสามารถระบุค่าที่แน่นอนที่ตัวแปรหรือ property สามารถมีได้ ทำให้เพิ่มความปลอดภัยของไทป์ (type safety) และป้องกันข้อผิดพลาดที่ไม่คาดคิด บทความนี้จะสำรวจ literal types อย่างลึกซึ้ง ครอบคลุมถึง синтаксис (syntax), การใช้งาน และประโยชน์ พร้อมตัวอย่างที่ใช้งานได้จริง

Literal Types คืออะไร?

แตกต่างจากไทป์แบบดั้งเดิมเช่น string, number, หรือ boolean, literal types ไม่ได้แทนหมวดหมู่ของค่าที่กว้างขวาง แต่กลับแทนค่าที่เฉพาะเจาะจงและตายตัว TypeScript รองรับ literal types 3 ประเภท:

ด้วยการใช้ literal types คุณสามารถสร้างคำจำกัดความของไทป์ที่แม่นยำยิ่งขึ้น ซึ่งสะท้อนถึงข้อจำกัดที่แท้จริงของข้อมูลของคุณ นำไปสู่โค้ดที่แข็งแกร่งและบำรุงรักษาง่ายขึ้น

String Literal Types

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

Syntax พื้นฐาน

Syntax สำหรับการกำหนด string literal type นั้นตรงไปตรงมา:


type AllowedValues = "value1" | "value2" | "value3";

โค้ดนี้กำหนดไทป์ชื่อ AllowedValues ที่สามารถมีค่าได้เพียงแค่สตริง "value1", "value2", หรือ "value3" เท่านั้น

ตัวอย่างการใช้งานจริง

1. การกำหนดชุดสี (Color Palette):

ลองจินตนาการว่าคุณกำลังสร้างไลบรารี UI และต้องการให้แน่ใจว่าผู้ใช้สามารถระบุสีจากชุดสีที่กำหนดไว้ล่วงหน้าเท่านั้น:


type Color = "red" | "green" | "blue" | "yellow";

function paintElement(element: HTMLElement, color: Color) {
  element.style.backgroundColor = color;
}

paintElement(document.getElementById("myElement")!, "red"); // ถูกต้อง
paintElement(document.getElementById("myElement")!, "purple"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"purple"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'Color' ได้

ตัวอย่างนี้แสดงให้เห็นว่า string literal types สามารถบังคับใช้ชุดของค่าที่อนุญาตได้อย่างเข้มงวด ป้องกันไม่ให้นักพัฒนาใช้สีที่ไม่ถูกต้องโดยไม่ตั้งใจ

2. การกำหนด API Endpoints:

เมื่อทำงานกับ API บ่อยครั้งที่คุณต้องระบุ endpoints ที่ได้รับอนุญาต String literal types สามารถช่วยบังคับใช้สิ่งนี้ได้:


type APIEndpoint = "/users" | "/posts" | "/comments";

function fetchData(endpoint: APIEndpoint) {
  // ... โค้ดสำหรับการดึงข้อมูลจาก endpoint ที่ระบุ
  console.log(`Fetching data from ${endpoint}`);
}

fetchData("/users"); // ถูกต้อง
fetchData("/products"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"/products"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'APIEndpoint' ได้

ตัวอย่างนี้ช่วยให้มั่นใจได้ว่าฟังก์ชัน fetchData สามารถเรียกใช้ได้ด้วย API endpoints ที่ถูกต้องเท่านั้น ลดความเสี่ยงของข้อผิดพลาดที่เกิดจากการพิมพ์ผิดหรือชื่อ endpoint ที่ไม่ถูกต้อง

3. การจัดการภาษาต่างๆ (Internationalization - i18n):

ในแอปพลิเคชันระดับโลก คุณอาจต้องจัดการกับภาษาต่างๆ คุณสามารถใช้ string literal types เพื่อให้แน่ใจว่าแอปพลิเคชันของคุณรองรับเฉพาะภาษาที่ระบุไว้เท่านั้น:


type Language = "en" | "es" | "fr" | "de" | "zh";

function translate(text: string, language: Language): string {
  // ... โค้ดสำหรับการแปลข้อความไปยังภาษาที่ระบุ
  console.log(`Translating '${text}' to ${language}`);
  return "Translated text"; // ค่าตัวอย่าง
}

translate("Hello", "en"); // ถูกต้อง
translate("Hello", "ja"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"ja"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'Language' ได้

ตัวอย่างนี้แสดงวิธีการตรวจสอบให้แน่ใจว่ามีการใช้เฉพาะภาษาที่รองรับภายในแอปพลิเคชันของคุณ

Number Literal Types

Number literal types ช่วยให้คุณสามารถระบุว่าตัวแปรหรือ property สามารถมีค่าเป็นตัวเลขที่เฉพาะเจาะจงได้เท่านั้น

Syntax พื้นฐาน

Syntax สำหรับการกำหนด number literal type นั้นคล้ายกับ string literal types:


type StatusCode = 200 | 404 | 500;

โค้ดนี้กำหนดไทป์ชื่อ StatusCode ที่สามารถมีค่าได้เพียงแค่ตัวเลข 200, 404, หรือ 500 เท่านั้น

ตัวอย่างการใช้งานจริง

1. การกำหนด HTTP Status Codes:

คุณสามารถใช้ number literal types เพื่อแทน HTTP status codes เพื่อให้แน่ใจว่ามีการใช้เฉพาะโค้ดที่ถูกต้องในแอปพลิเคชันของคุณ:


type HTTPStatus = 200 | 400 | 401 | 403 | 404 | 500;

function handleResponse(status: HTTPStatus) {
  switch (status) {
    case 200:
      console.log("Success!");
      break;
    case 400:
      console.log("Bad Request");
      break;
    // ... กรณีอื่นๆ
    default:
      console.log("Unknown Status");
  }
}

handleResponse(200); // ถูกต้อง
handleResponse(600); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '600' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'HTTPStatus' ได้

ตัวอย่างนี้บังคับให้ใช้ HTTP status codes ที่ถูกต้อง ป้องกันข้อผิดพลาดที่เกิดจากการใช้โค้ดที่ไม่ถูกต้องหรือไม่เป็นมาตรฐาน

2. การแทนค่าตัวเลือกที่ตายตัว:

คุณสามารถใช้ number literal types เพื่อแทนตัวเลือกที่ตายตัวในอ็อบเจกต์การกำหนดค่า (configuration object):


type RetryAttempts = 1 | 3 | 5;

interface Config {
  retryAttempts: RetryAttempts;
}

const config1: Config = { retryAttempts: 3 }; // ถูกต้อง
const config2: Config = { retryAttempts: 7 }; // ข้อผิดพลาด: ไทป์ '{ retryAttempts: 7; }' ไม่สามารถกำหนดให้กับไทป์ 'Config' ได้

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

Boolean Literal Types

Boolean literal types แทนค่าเฉพาะ true หรือ false แม้ว่าอาจดูเหมือนมีความยืดหยุ่นน้อยกว่า string หรือ number literal types แต่ก็มีประโยชน์ในสถานการณ์เฉพาะ

Syntax พื้นฐาน

Syntax สำหรับการกำหนด boolean literal type คือ:


type IsEnabled = true | false;

อย่างไรก็ตาม การใช้ true | false โดยตรงนั้นซ้ำซ้อน เพราะมันเทียบเท่ากับไทป์ boolean อยู่แล้ว Boolean literal types มีประโยชน์มากกว่าเมื่อใช้ร่วมกับไทป์อื่น ๆ หรือใน conditional types

ตัวอย่างการใช้งานจริง

1. ตรรกะแบบมีเงื่อนไขกับการกำหนดค่า:

คุณสามารถใช้ boolean literal types เพื่อควบคุมการทำงานของฟังก์ชันตามค่าสถานะ (flag) ของการกำหนดค่า:


interface FeatureFlags {
  darkMode: boolean;
  newUserFlow: boolean;
}

function initializeApp(flags: FeatureFlags) {
  if (flags.darkMode) {
    // เปิดใช้งาน dark mode
    console.log("Enabling dark mode...");
  } else {
    // ใช้ light mode
    console.log("Using light mode...");
  }

  if (flags.newUserFlow) {
    // เปิดใช้งานขั้นตอนสำหรับผู้ใช้ใหม่
    console.log("Enabling new user flow...");
  } else {
    // ใช้ขั้นตอนสำหรับผู้ใช้เก่า
    console.log("Using old user flow...");
  }
}

initializeApp({ darkMode: true, newUserFlow: false });

แม้ว่าตัวอย่างนี้จะใช้ไทป์ boolean มาตรฐาน แต่คุณสามารถรวมมันเข้ากับ conditional types (จะอธิบายในภายหลัง) เพื่อสร้างพฤติกรรมที่ซับซ้อนยิ่งขึ้น

2. Discriminated Unions:

Boolean literal types สามารถใช้เป็นตัวจำแนก (discriminator) ใน union types ได้ พิจารณาตัวอย่างต่อไปนี้:


interface SuccessResult {
  success: true;
  data: any;
}

interface ErrorResult {
  success: false;
  error: string;
}

type Result = SuccessResult | ErrorResult;

function processResult(result: Result) {
  if (result.success) {
    console.log("Success:", result.data);
  } else {
    console.error("Error:", result.error);
  }
}

processResult({ success: true, data: { name: "John" } });
processResult({ success: false, error: "Failed to fetch data" });

ในที่นี้ property success ซึ่งเป็น boolean literal type ทำหน้าที่เป็นตัวจำแนก ทำให้ TypeScript สามารถจำกัดขอบเขต (narrow down) ไทป์ของ result ภายในคำสั่ง if ได้

การรวม Literal Types กับ Union Types

Literal types จะทรงพลังที่สุดเมื่อใช้ร่วมกับ union types (โดยใช้ตัวดำเนินการ |) ซึ่งช่วยให้คุณสามารถกำหนดไทป์ที่สามารถมีค่าได้หนึ่งในหลาย ๆ ค่าที่เฉพาะเจาะจง

ตัวอย่างการใช้งานจริง

1. การกำหนดไทป์สถานะ (Status Type):


type Status = "pending" | "in progress" | "completed" | "failed";

interface Task {
  id: number;
  description: string;
  status: Status;
}

const task1: Task = { id: 1, description: "Implement login", status: "in progress" }; // ถูกต้อง
const task2: Task = { id: 2, description: "Implement logout", status: "done" };       // ข้อผิดพลาด: ไทป์ '{ id: number; description: string; status: string; }' ไม่สามารถกำหนดให้กับไทป์ 'Task' ได้

ตัวอย่างนี้แสดงวิธีการบังคับใช้ชุดของค่าสถานะที่อนุญาตสำหรับอ็อบเจกต์ Task

2. การกำหนดไทป์อุปกรณ์ (Device Type):

ในแอปพลิเคชันมือถือ คุณอาจต้องจัดการกับประเภทอุปกรณ์ที่แตกต่างกัน คุณสามารถใช้ union ของ string literal types เพื่อแทนสิ่งเหล่านี้ได้:


type DeviceType = "mobile" | "tablet" | "desktop";

function logDeviceType(device: DeviceType) {
  console.log(`Device type: ${device}`);
}

logDeviceType("mobile"); // ถูกต้อง
logDeviceType("smartwatch"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"smartwatch"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'DeviceType' ได้

ตัวอย่างนี้ช่วยให้มั่นใจได้ว่าฟังก์ชัน logDeviceType จะถูกเรียกใช้ด้วยประเภทอุปกรณ์ที่ถูกต้องเท่านั้น

Literal Types กับ Type Aliases

Type aliases (โดยใช้คีย์เวิร์ด type) เป็นวิธีในการตั้งชื่อให้กับ literal type ทำให้โค้ดของคุณอ่านง่ายและบำรุงรักษาได้ง่ายขึ้น

ตัวอย่างการใช้งานจริง

1. การกำหนดไทป์รหัสสกุลเงิน (Currency Code Type):


type CurrencyCode = "USD" | "EUR" | "GBP" | "JPY";

function formatCurrency(amount: number, currency: CurrencyCode): string {
  // ... โค้ดสำหรับการจัดรูปแบบจำนวนเงินตามรหัสสกุลเงิน
  console.log(`Formatting ${amount} in ${currency}`);
  return "Formatted amount"; // ค่าตัวอย่าง
}

formatCurrency(100, "USD"); // ถูกต้อง
formatCurrency(200, "CAD"); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"CAD"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'CurrencyCode' ได้

ตัวอย่างนี้กำหนด type alias ชื่อ CurrencyCode สำหรับชุดของรหัสสกุลเงิน ซึ่งช่วยปรับปรุงความสามารถในการอ่านของฟังก์ชัน formatCurrency

2. การกำหนดไทป์วันในสัปดาห์ (Day of the Week Type):


type DayOfWeek = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday";

function isWeekend(day: DayOfWeek): boolean {
  return day === "Saturday" || day === "Sunday";
}

console.log(isWeekend("Monday"));   // false
console.log(isWeekend("Saturday")); // true
console.log(isWeekend("Funday"));   // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '"Funday"' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท 'DayOfWeek' ได้

การอนุมาน Literal (Literal Inference)

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

ตัวอย่างการใช้งานจริง

1. การอนุมาน String Literal Types:


const apiKey = "your-api-key"; // TypeScript อนุมานไทป์ของ apiKey เป็น "your-api-key"

function validateApiKey(key: "your-api-key") {
  return key === "your-api-key";
}

console.log(validateApiKey(apiKey)); // true

const anotherKey = "invalid-key";
console.log(validateApiKey(anotherKey)); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท 'string' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท '"your-api-key"' ได้

ในตัวอย่างนี้ TypeScript อนุมานไทป์ของ apiKey เป็น string literal type "your-api-key" อย่างไรก็ตาม หากคุณกำหนดค่าที่ไม่ใช่ค่าคงที่ (non-constant) ให้กับตัวแปร TypeScript มักจะอนุมานเป็นไทป์ string ที่กว้างกว่า

2. การอนุมาน Number Literal Types:


const port = 8080; // TypeScript อนุมานไทป์ของ port เป็น 8080

function startServer(portNumber: 8080) {
  console.log(`Starting server on port ${portNumber}`);
}

startServer(port); // ถูกต้อง

const anotherPort = 3000;
startServer(anotherPort); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท 'number' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท '8080' ได้

การใช้ Literal Types กับ Conditional Types

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

Syntax พื้นฐาน

Syntax สำหรับ conditional type คือ:


TypeA extends TypeB ? TypeC : TypeD

ซึ่งหมายความว่า: ถ้า TypeA สามารถกำหนดให้กับ TypeB ได้ ผลลัพธ์ของไทป์จะเป็น TypeC; มิฉะนั้น ผลลัพธ์ของไทป์จะเป็น TypeD

ตัวอย่างการใช้งานจริง

1. การจับคู่สถานะกับข้อความ (Mapping Status to Message):


type Status = "pending" | "in progress" | "completed" | "failed";

type StatusMessage = T extends "pending"
  ? "Waiting for action"
  : T extends "in progress"
  ? "Currently processing"
  : T extends "completed"
  ? "Task finished successfully"
  : "An error occurred";

function getStatusMessage(status: T): StatusMessage {
  switch (status) {
    case "pending":
      return "Waiting for action" as StatusMessage;
    case "in progress":
      return "Currently processing" as StatusMessage;
    case "completed":
      return "Task finished successfully" as StatusMessage;
    case "failed":
      return "An error occurred" as StatusMessage;
    default:
      throw new Error("Invalid status");
  }
}

console.log(getStatusMessage("pending"));    // Waiting for action
console.log(getStatusMessage("in progress")); // Currently processing
console.log(getStatusMessage("completed"));   // Task finished successfully
console.log(getStatusMessage("failed"));      // An error occurred

ตัวอย่างนี้กำหนดไทป์ StatusMessage ที่จับคู่แต่ละสถานะที่เป็นไปได้กับข้อความที่สอดคล้องกันโดยใช้ conditional types ฟังก์ชัน getStatusMessage ใช้ประโยชน์จากไทป์นี้เพื่อให้ข้อความสถานะมีความปลอดภัยทางไทป์ (type-safe)

2. การสร้างตัวจัดการอีเวนต์ที่ปลอดภัยทางไทป์ (Type-Safe Event Handler):


type EventType = "click" | "mouseover" | "keydown";

type EventData = T extends "click"
  ? { x: number; y: number; } // ข้อมูลอีเวนต์ Click
  : T extends "mouseover"
  ? { target: HTMLElement; }   // ข้อมูลอีเวนต์ Mouseover
  : { key: string; }             // ข้อมูลอีเวนต์ Keydown

function handleEvent(type: T, data: EventData) {
  console.log(`Handling event type ${type} with data:`, data);
}

handleEvent("click", { x: 10, y: 20 }); // ถูกต้อง
handleEvent("mouseover", { target: document.getElementById("myElement")! }); // ถูกต้อง
handleEvent("keydown", { key: "Enter" }); // ถูกต้อง

handleEvent("click", { key: "Enter" }); // ข้อผิดพลาด: อาร์กิวเมนต์ประเภท '{ key: string; }' ไม่สามารถกำหนดให้กับพารามิเตอร์ประเภท '{ x: number; y: number; }' ได้

ตัวอย่างนี้สร้างไทป์ EventData ที่กำหนดโครงสร้างข้อมูลที่แตกต่างกันตามประเภทของอีเวนต์ ซึ่งช่วยให้คุณมั่นใจได้ว่าข้อมูลที่ถูกต้องจะถูกส่งไปยังฟังก์ชัน handleEvent สำหรับแต่ละประเภทของอีเวนต์

แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ Literal Types

เพื่อให้การใช้ literal types ในโปรเจกต์ TypeScript ของคุณมีประสิทธิภาพสูงสุด โปรดพิจารณาแนวทางปฏิบัติต่อไปนี้:

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

สรุป

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

TypeScript Literal Types: การควบคุมข้อจำกัดของค่าที่แน่นอนอย่างเชี่ยวชาญ | MLOG