ไทย

ปลดล็อกพลังของโครงสร้างข้อมูลแบบ Immutable ใน TypeScript ด้วย Readonly Types เรียนรู้วิธีสร้างแอปพลิเคชันที่คาดการณ์ได้ง่าย บำรุงรักษาได้ดี และแข็งแกร่งยิ่งขึ้น โดยการป้องกันการเปลี่ยนแปลงข้อมูลที่ไม่ตั้งใจ

TypeScript Readonly Types: การเรียนรู้โครงสร้างข้อมูลแบบ Immutable

ในโลกของการพัฒนาซอฟต์แวร์ที่เปลี่ยนแปลงอยู่เสมอ การสร้างโค้ดที่แข็งแกร่ง คาดการณ์ได้ และบำรุงรักษาง่ายเป็นเป้าหมายสำคัญ TypeScript ซึ่งมีระบบ Type ที่เข้มแข็ง ได้มอบเครื่องมืออันทรงพลังเพื่อบรรลุเป้าหมายเหล่านี้ ในบรรดาเครื่องมือเหล่านั้น readonly types ถือเป็นกลไกสำคัญในการบังคับใช้ Immutability ซึ่งเป็นรากฐานของการเขียนโปรแกรมเชิงฟังก์ชัน (Functional Programming) และเป็นกุญแจสำคัญในการสร้างแอปพลิเคชันที่น่าเชื่อถือยิ่งขึ้น

Immutability คืออะไร และทำไมจึงสำคัญ?

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

Readonly Types ใน TypeScript: คลังอาวุธสำหรับ Immutability ของคุณ

TypeScript มีหลายวิธีในการบังคับใช้ Immutability โดยใช้คีย์เวิร์ด readonly เรามาสำรวจเทคนิคต่างๆ และวิธีการนำไปใช้จริงกัน

1. Readonly Properties บน Interfaces และ Types

วิธีที่ตรงไปตรงมาที่สุดในการประกาศ property ให้เป็นแบบอ่านอย่างเดียวคือการใช้คีย์เวิร์ด readonly โดยตรงในนิยามของ interface หรือ type


interface Person {
  readonly id: string;
  name: string;
  age: number;
}

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'id' ได้เนื่องจากเป็น property แบบอ่านอย่างเดียว
person.name = "Bob"; // แบบนี้สามารถทำได้

ในตัวอย่างนี้ property id ถูกประกาศเป็น readonly TypeScript จะป้องกันความพยายามใดๆ ที่จะแก้ไขค่านี้หลังจากที่อ็อบเจกต์ถูกสร้างขึ้น ส่วน property name และ age ที่ไม่มี readonly modifier สามารถแก้ไขได้อย่างอิสระ

2. Utility Type Readonly

TypeScript มี Utility Type ที่ทรงพลังชื่อว่า Readonly<T> ซึ่งเป็น Generic Type ที่รับ Type ที่มีอยู่แล้ว T เข้ามาและแปลงค่าโดยทำให้ property ทั้งหมดของมันเป็น readonly


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'x' ได้เนื่องจากเป็น property แบบอ่านอย่างเดียว

Type Readonly<Point> จะสร้าง Type ใหม่ที่ทั้ง x และ y เป็น readonly นี่เป็นวิธีที่สะดวกในการทำให้ Type ที่มีอยู่แล้วกลายเป็น Immutable ได้อย่างรวดเร็ว

3. Readonly Arrays (ReadonlyArray<T>) และ readonly T[]

โดยธรรมชาติแล้ว Array ใน JavaScript สามารถเปลี่ยนแปลงค่าได้ TypeScript มีวิธีสร้าง Array แบบอ่านอย่างเดียวโดยใช้ Type ReadonlyArray<T> หรือรูปแบบย่อ readonly T[] ซึ่งจะป้องกันการแก้ไขเนื้อหาภายใน Array


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // ข้อผิดพลาด: Property 'push' ไม่มีอยู่บน type 'readonly number[]'.
// numbers[0] = 10; // ข้อผิดพลาด: Index signature ใน type 'readonly number[]' อนุญาตให้อ่านได้อย่างเดียว

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // เทียบเท่ากับ ReadonlyArray
// moreNumbers.push(11); // ข้อผิดพลาด: Property 'push' ไม่มีอยู่บน type 'readonly number[]'.

การพยายามใช้เมธอดที่แก้ไข Array เช่น push, pop, splice หรือการกำหนดค่าให้กับ index โดยตรง จะทำให้เกิดข้อผิดพลาดใน TypeScript

4. const กับ readonly: ทำความเข้าใจความแตกต่าง

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


const immutableNumber = 42;
// immutableNumber = 43; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าใหม่ให้ตัวแปร const 'immutableNumber' ได้

const mutableObject = { value: 10 };
mutableObject.value = 20; // แบบนี้ทำได้เพราะ *อ็อบเจกต์* ไม่ได้เป็น const มีเพียงตัวแปรที่เป็น const

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'value' ได้เนื่องจากเป็น property แบบอ่านอย่างเดียว

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าใหม่ให้ตัวแปร const 'constReadonlyObject' ได้
// constReadonlyObject.value = 60; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'value' ได้เนื่องจากเป็น property แบบอ่านอย่างเดียว

ดังที่แสดงในตัวอย่างข้างต้น const ทำให้แน่ใจว่าตัวแปรจะชี้ไปยังอ็อบเจกต์เดิมในหน่วยความจำเสมอ ในขณะที่ readonly รับประกันว่าสถานะภายในของอ็อบเจกต์จะไม่เปลี่ยนแปลง

ตัวอย่างการใช้งานจริง: การประยุกต์ใช้ Readonly Types ในสถานการณ์ต่างๆ

เรามาดูตัวอย่างการใช้งานจริงบางส่วนของ Readonly Types ที่สามารถช่วยเพิ่มคุณภาพโค้ดและการบำรุงรักษาในสถานการณ์ต่างๆ

1. การจัดการข้อมูล Configuration

ข้อมูล Configuration มักจะถูกโหลดเพียงครั้งเดียวเมื่อแอปพลิเคชันเริ่มต้น และไม่ควรถูกแก้ไขในระหว่างการทำงาน การใช้ Readonly Types จะช่วยให้มั่นใจว่าข้อมูลนี้ยังคงสอดคล้องกันและป้องกันการแก้ไขโดยไม่ได้ตั้งใจ


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... ใช้ config.timeout และ config.apiUrl ได้อย่างปลอดภัย โดยรู้ว่าจะไม่มีการเปลี่ยนแปลง
}

fetchData("/data", config);

2. การพัฒนาระบบจัดการ State แบบ Redux

ในไลบรารีการจัดการ State เช่น Redux นั้น Immutability ถือเป็นหลักการสำคัญ Readonly Types สามารถใช้เพื่อรับประกันว่า State จะยังคงเป็น Immutable และ Reducer จะคืนค่าเป็น State object ใหม่เสมอ แทนที่จะแก้ไข State ที่มีอยู่เดิม


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // คืนค่า state object ใหม่
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // คืนค่า state object ใหม่พร้อมกับ items ที่อัปเดตแล้ว
    default:
      return state;
  }
}

3. การทำงานกับข้อมูลที่ได้จาก API

เมื่อดึงข้อมูลจาก API เรามักจะต้องการให้ข้อมูลที่ตอบกลับมานั้นเป็นแบบ Immutable โดยเฉพาะอย่างยิ่งหากคุณใช้ข้อมูลนั้นในการเรนเดอร์ส่วนประกอบ UI Readonly Types สามารถช่วยป้องกันการเปลี่ยนแปลงข้อมูล API โดยไม่ได้ตั้งใจได้


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'completed' ได้เนื่องจากเป็น property แบบอ่านอย่างเดียว
});

4. การสร้างโมเดลข้อมูลทางภูมิศาสตร์ (ตัวอย่างระหว่างประเทศ)

ลองพิจารณาการแสดงพิกัดทางภูมิศาสตร์ เมื่อกำหนดพิกัดแล้ว โดยหลักการแล้วพิกัดควรจะคงที่ สิ่งนี้ช่วยรับประกันความสมบูรณ์ของข้อมูล โดยเฉพาะอย่างยิ่งเมื่อต้องจัดการกับแอปพลิเคชันที่ละเอียดอ่อน เช่น ระบบแผนที่หรือระบบนำทางที่ทำงานในภูมิภาคต่างๆ (เช่น พิกัด GPS สำหรับบริการจัดส่งที่ครอบคลุมอเมริกาเหนือ ยุโรป และเอเชีย)


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // สมมติว่าเป็นการคำนวณที่ซับซ้อนโดยใช้ละติจูดและลองจิจูด
 // คืนค่า placeholder เพื่อความเรียบง่าย
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Distance between Tokyo and New York (placeholder):", distance);

// tokyoCoordinates.latitude = 36.0; // ข้อผิดพลาด: ไม่สามารถกำหนดค่าให้ 'latitude' ได้เนื่องจากเป็น property แบบอ่านอย่างเดียว

Deeply Readonly Types: การจัดการกับอ็อบเจกต์ที่ซ้อนกัน

Utility Type Readonly<T> จะทำให้ property โดยตรงของอ็อบเจกต์เป็น readonly เท่านั้น หากอ็อบเจกต์มีอ็อบเจกต์หรืออาร์เรย์ซ้อนกันอยู่ โครงสร้างที่ซ้อนกันเหล่านั้นจะยังคงสามารถเปลี่ยนแปลงได้ เพื่อให้ได้ Immutability ที่ลึกลงไปอย่างแท้จริง คุณต้องใช้ Readonly<T> กับ property ที่ซ้อนกันทั้งหมดแบบเวียนเกิด (Recursively)

นี่คือตัวอย่างวิธีการสร้าง Type ที่เป็น Readonly แบบลึก:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // ข้อผิดพลาด
// company.address.city = "New City"; // ข้อผิดพลาด
// company.employees.push("Charlie"); // ข้อผิดพลาด

Type DeepReadonly<T> นี้จะใช้ Readonly<T> กับ property ที่ซ้อนกันทั้งหมดแบบเวียนเกิด เพื่อให้แน่ใจว่าโครงสร้างอ็อบเจกต์ทั้งหมดเป็นแบบ Immutable

ข้อควรพิจารณาและข้อดีข้อเสีย

แม้ว่า Immutability จะมีประโยชน์อย่างมาก แต่ก็เป็นเรื่องสำคัญที่จะต้องตระหนักถึงข้อดีข้อเสียที่อาจเกิดขึ้น

ไลบรารีสำหรับโครงสร้างข้อมูลแบบ Immutable

มีไลบรารีหลายตัวที่สามารถช่วยให้การทำงานกับโครงสร้างข้อมูลแบบ Immutable ใน TypeScript ง่ายขึ้น:

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

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

สรุป: การนำ Immutability มาใช้ด้วย TypeScript Readonly Types

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