ไทย

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

คู่มือฉบับสมบูรณ์: การใช้ Mapped Types ใน TypeScript เพื่อการแปลงอ็อบเจกต์แบบไดนามิก

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

การทำความเข้าใจแนวคิดหลัก

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

มาเริ่มจากพื้นฐานกันก่อน ลองพิจารณา interface ง่ายๆ นี้:

interface Person {
  name: string;
  age: number;
  email: string;
}

ตอนนี้ เราจะมาสร้าง mapped type ที่ทำให้ property ทั้งหมดของ Person เป็น optional:

type OptionalPerson = { 
  [K in keyof Person]?: Person[K];
};

ในตัวอย่างนี้:

ไทป์ OptionalPerson ที่ได้จะมีลักษณะดังนี้:

{
  name?: string;
  age?: number;
  email?: string;
}

นี่คือตัวอย่างที่แสดงให้เห็นถึงพลังของ mapped types ในการแก้ไขไทป์ที่มีอยู่แบบไดนามิก

ไวยากรณ์และโครงสร้างของ Mapped Types

ไวยากรณ์ของ mapped type นั้นค่อนข้างเฉพาะเจาะจงและมีโครงสร้างทั่วไปดังนี้:

type NewType = { 
  [Key in KeysType]: ValueType;
};

มาดูส่วนประกอบแต่ละส่วนกัน:

ตัวอย่าง: การแปลงประเภทของ Property

สมมติว่าคุณต้องการแปลง property ที่เป็นตัวเลขทั้งหมดของอ็อบเจกต์ให้เป็นสตริง คุณสามารถทำได้โดยใช้ mapped type ดังนี้:

interface Product {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

type StringifiedProduct = {
  [K in keyof Product]: Product[K] extends number ? string : Product[K];
};

ในกรณีนี้ เรากำลัง:

ไทป์ StringifiedProduct ที่ได้จะเป็น:

{
  id: string;
  name: string;
  price: string;
  quantity: string;
}

คุณสมบัติและเทคนิคที่สำคัญ

1. การใช้ keyof และ Index Signatures

ดังที่แสดงให้เห็นก่อนหน้านี้ keyof เป็นเครื่องมือพื้นฐานสำหรับการทำงานกับ mapped types ซึ่งช่วยให้คุณสามารถวนซ้ำคีย์ของไทป์ได้ ส่วน Index Signatures เป็นวิธีในการกำหนดไทป์ของ property เมื่อคุณไม่ทราบคีย์ล่วงหน้า แต่ยังต้องการแปลงค่าเหล่านั้น

ตัวอย่าง: การแปลง Property ทั้งหมดโดยใช้ Index Signature

interface StringMap {
  [key: string]: number;
}

type StringMapToString = {
  [K in keyof StringMap]: string;
};

ในที่นี้ ค่าที่เป็นตัวเลขทั้งหมดใน StringMap จะถูกแปลงเป็นสตริงในไทป์ใหม่

2. Conditional Types ภายใน Mapped Types

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

ตัวอย่าง: การลบ Null และ Undefined ออกจากไทป์

type NonNullableProperties = {
  [K in keyof T]: T[K] extends (null | undefined) ? never : T[K];
};

mapped type นี้จะวนซ้ำคีย์ทั้งหมดของไทป์ T และใช้ conditional type เพื่อตรวจสอบว่าค่านั้นอนุญาตให้เป็น null หรือ undefined หรือไม่ ถ้าใช่ ไทป์จะถูกประเมินเป็น never ซึ่งเป็นการลบ property นั้นออกไปอย่างมีประสิทธิภาพ มิฉะนั้นจะคงไทป์เดิมไว้ วิธีนี้ทำให้ไทป์มีความแข็งแกร่งมากขึ้นโดยการกำจัดค่า null หรือ undefined ที่อาจก่อปัญหา ซึ่งช่วยปรับปรุงคุณภาพของโค้ดและสอดคล้องกับแนวปฏิบัติที่ดีที่สุดสำหรับการพัฒนาซอฟต์แวร์ระดับโลก

3. Utility Types เพื่อประสิทธิภาพ

TypeScript มี utility types ในตัวที่ช่วยลดความซับซ้อนของงานจัดการไทป์ทั่วไป ซึ่งไทป์เหล่านี้ใช้ mapped types อยู่เบื้องหลัง

ตัวอย่าง: การใช้ Pick และ Omit

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

type UserSummary = Pick;
// { id: number; name: string; }

type UserWithoutEmail = Omit;
// { id: number; name: string; role: string; }

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

การประยุกต์ใช้และตัวอย่างในโลกจริง

1. การตรวจสอบและแปลงข้อมูล

Mapped types มีคุณค่าอย่างยิ่งสำหรับการตรวจสอบและแปลงข้อมูลที่ได้รับจากแหล่งภายนอก (เช่น API, ฐานข้อมูล, ข้อมูลที่ผู้ใช้ป้อน) นี่เป็นสิ่งสำคัญในแอปพลิเคชันระดับโลกที่คุณอาจต้องจัดการกับข้อมูลจากหลายแหล่งที่แตกต่างกันและจำเป็นต้องรับประกันความสมบูรณ์ของข้อมูล โดยช่วยให้คุณสามารถกำหนดกฎเฉพาะ เช่น การตรวจสอบชนิดข้อมูล และแก้ไขโครงสร้างข้อมูลโดยอัตโนมัติตามกฎเหล่านั้น

ตัวอย่าง: การแปลงข้อมูลตอบกลับจาก API

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

type CleanedApiResponse = {
  [K in keyof ApiResponse]:
    K extends 'userId' | 'id' ? number :
    K extends 'title' ? string :
    K extends 'completed' ? boolean : any;
};

ตัวอย่างนี้แปลง property userId และ id (ซึ่งเดิมเป็นสตริงจาก API) ให้เป็นตัวเลข ส่วน property title ถูกกำหนดไทป์เป็นสตริงอย่างถูกต้อง และ completed ยังคงเป็น boolean ซึ่งช่วยรับประกันความสอดคล้องของข้อมูลและหลีกเลี่ยงข้อผิดพลาดที่อาจเกิดขึ้นในการประมวลผลถัดไป

2. การสร้าง Props สำหรับคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้

ใน React และ UI framework อื่นๆ mapped types สามารถทำให้การสร้าง props สำหรับคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้ง่ายขึ้น ซึ่งมีความสำคัญอย่างยิ่งเมื่อพัฒนา UI component ระดับโลกที่ต้องปรับให้เข้ากับภาษาและส่วนติดต่อผู้ใช้ที่แตกต่างกัน

ตัวอย่าง: การจัดการ Localization

interface TextProps {
  textId: string;
  defaultText: string;
  locale: string;
}

type LocalizedTextProps = {
  [K in keyof TextProps as `localized-${K}`]: TextProps[K];
};

ในโค้ดนี้ ไทป์ใหม่ LocalizedTextProps จะเพิ่มคำนำหน้าให้กับชื่อ property แต่ละตัวของ TextProps ตัวอย่างเช่น textId จะกลายเป็น localized-textId ซึ่งมีประโยชน์สำหรับการตั้งค่า props ของคอมโพเนนต์ รูปแบบนี้สามารถใช้เพื่อสร้าง props ที่อนุญาตให้เปลี่ยนข้อความแบบไดนามิกตามภาษาของผู้ใช้ ซึ่งจำเป็นอย่างยิ่งสำหรับการสร้างส่วนติดต่อผู้ใช้หลายภาษาที่ทำงานได้อย่างราบรื่นในภูมิภาคและภาษาต่างๆ เช่น ในแอปพลิเคชันอีคอมเมิร์ซหรือแพลตฟอร์มโซเชียลมีเดียระหว่างประเทศ props ที่ถูกแปลงแล้วจะช่วยให้นักพัฒนาสามารถควบคุม localization ได้มากขึ้นและสามารถสร้างประสบการณ์ผู้ใช้ที่สอดคล้องกันทั่วโลกได้

3. การสร้างฟอร์มแบบไดนามิก

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

ตัวอย่าง: การสร้างฟิลด์ฟอร์มอัตโนมัติตามคีย์ของอ็อบเจกต์

interface UserProfile {
  firstName: string;
  lastName: string;
  email: string;
  phoneNumber: string;
}

type FormFields = {
  [K in keyof UserProfile]: {
    label: string;
    type: string;
    required: boolean;
  };
};

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

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

1. การแมปคีย์ใหม่ (Key Remapping)

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

ตัวอย่าง: การเปลี่ยนชื่อ Property

interface Product {
  productId: number;
  productName: string;
  productDescription: string;
  price: number;
}

type ProductDto = {
  [K in keyof Product as `dto_${K}`]: Product[K];
};

โค้ดนี้จะเปลี่ยนชื่อ property แต่ละตัวของไทป์ Product ให้ขึ้นต้นด้วย dto_ ซึ่งมีประโยชน์อย่างยิ่งเมื่อทำการแมประหว่างโมเดลข้อมูลและ API ที่ใช้รูปแบบการตั้งชื่อที่แตกต่างกัน และมีความสำคัญในการพัฒนาซอฟต์แวร์ระหว่างประเทศที่แอปพลิเคชันต้องเชื่อมต่อกับระบบหลังบ้านหลายระบบซึ่งอาจมีรูปแบบการตั้งชื่อเฉพาะ ทำให้การรวมระบบเป็นไปอย่างราบรื่น

2. การแมปคีย์ใหม่แบบมีเงื่อนไข

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

ตัวอย่าง: การยกเว้น Property ออกจาก DTO


interface Product {
    id: number;
    name: string;
    description: string;
    price: number;
    category: string;
    isActive: boolean;
}

type ProductDto = {
    [K in keyof Product as K extends 'description' | 'isActive' ? never : K]: Product[K]
}

ในที่นี้ property description และ isActive จะถูกลบออกจากไทป์ ProductDto ที่สร้างขึ้นอย่างมีประสิทธิภาพ เนื่องจากคีย์จะถูกประเมินเป็น never หาก property นั้นคือ 'description' หรือ 'isActive' ซึ่งช่วยให้สามารถสร้าง Data Transfer Objects (DTOs) เฉพาะที่มีเฉพาะข้อมูลที่จำเป็นสำหรับการดำเนินการต่างๆ การถ่ายโอนข้อมูลแบบเลือกเช่นนี้มีความสำคัญต่อการเพิ่มประสิทธิภาพและความเป็นส่วนตัวในแอปพลิเคชันระดับโลก ข้อจำกัดในการถ่ายโอนข้อมูลช่วยให้มั่นใจได้ว่ามีเพียงข้อมูลที่เกี่ยวข้องเท่านั้นที่ถูกส่งผ่านเครือข่าย ซึ่งช่วยลดการใช้แบนด์วิดท์และปรับปรุงประสบการณ์ผู้ใช้ให้ดีขึ้น ซึ่งสอดคล้องกับกฎระเบียบด้านความเป็นส่วนตัวทั่วโลก

3. การใช้ Mapped Types ร่วมกับ Generics

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

ตัวอย่าง: ฟังก์ชัน Generic สำหรับการแปลงค่า Property ของอ็อบเจกต์


function transformObjectValues(obj: T, transform: (value: T[K]) => U): {
    [P in keyof T]: U;
} {
    const result: any = {};
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            result[key] = transform(obj[key]);
        }
    }
    return result;
}

interface Order {
    id: number;
    items: string[];
    total: number;
}

const order: Order = {
    id: 123,
    items: ['apple', 'banana'],
    total: 5.99,
};

const stringifiedOrder = transformObjectValues(order, (value) => String(value));
// stringifiedOrder: { id: string; items: string; total: string; }

ในตัวอย่างนี้ ฟังก์ชัน transformObjectValues ใช้ generics (T, K, และ U) เพื่อรับอ็อบเจกต์ (obj) ที่มีไทป์ T และฟังก์ชัน transform ที่รับ property เดียวจาก T และคืนค่าที่มีไทป์ U จากนั้นฟังก์ชันจะคืนค่าเป็นอ็อบเจกต์ใหม่ที่มีคีย์เหมือนกับอ็อบเจกต์เดิม แต่มีค่าที่ถูกแปลงเป็นไทป์ U แล้ว

แนวปฏิบัติที่ดีที่สุดและข้อควรพิจารณา

1. ความปลอดภัยของไทป์และการบำรุงรักษาโค้ด

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

2. ความสามารถในการอ่านและสไตล์ของโค้ด

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

3. การใช้งานมากเกินไปและความซับซ้อน

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

4. ประสิทธิภาพ

Mapped types ส่วนใหญ่มีผลต่อการตรวจสอบไทป์ในขณะคอมไพล์และโดยทั่วไปไม่ได้สร้างภาระด้านประสิทธิภาพขณะรันไทม์อย่างมีนัยสำคัญ อย่างไรก็ตาม การจัดการไทป์ที่ซับซ้อนเกินไปอาจทำให้กระบวนการคอมไพล์ช้าลงได้ ควรลดความซับซ้อนและพิจารณาผลกระทบต่อเวลาในการ build โดยเฉพาะในโครงการขนาดใหญ่หรือสำหรับทีมที่กระจายตัวอยู่ตามเขตเวลาต่างๆ และมีข้อจำกัดด้านทรัพยากรที่แตกต่างกัน

สรุป

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