ไทย

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

การเรียนรู้ TypeScript Infer อย่างเชี่ยวชาญ: การดึงชนิดข้อมูลแบบมีเงื่อนไขสำหรับการจัดการชนิดข้อมูลขั้นสูง

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

ชนิดข้อมูลแบบมีเงื่อนไขคืออะไร

ก่อนที่เราจะดำดิ่งสู่ infer มาทบทวนชนิดข้อมูลแบบมีเงื่อนไขกันอย่างรวดเร็ว ชนิดข้อมูลแบบมีเงื่อนไขใน TypeScript ช่วยให้คุณกำหนดชนิดข้อมูลตามเงื่อนไข คล้ายกับตัวดำเนินการ ternary ใน JavaScript ไวยากรณ์พื้นฐานคือ:

T extends U ? X : Y

ซึ่งอ่านได้ว่า: "ถ้าชนิดข้อมูล T สามารถกำหนดให้กับชนิดข้อมูล U ได้ ชนิดข้อมูลจะเป็น X มิฉะนั้น ชนิดข้อมูลจะเป็น Y"

ตัวอย่าง:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false

แนะนำคีย์เวิร์ด infer

คีย์เวิร์ด infer ใช้ภายใน clause extends ของชนิดข้อมูลแบบมีเงื่อนไขเพื่อประกาศตัวแปรชนิดข้อมูลที่สามารถอนุมานได้จากชนิดข้อมูลที่กำลังตรวจสอบ โดยพื้นฐานแล้ว จะช่วยให้คุณ "จับภาพ" ส่วนหนึ่งของชนิดข้อมูลเพื่อใช้ในภายหลัง

ไวยากรณ์พื้นฐาน:

type MyType<T> = T extends (infer U) ? U : never;

ในตัวอย่างนี้ ถ้า T สามารถกำหนดให้กับชนิดข้อมูลบางชนิดได้ TypeScript จะพยายามอนุมานชนิดข้อมูลของ U หากการอนุมานสำเร็จ ชนิดข้อมูลจะเป็น U มิฉะนั้น จะเป็น never

ตัวอย่างง่ายๆ ของ infer

1. การอนุมานชนิดข้อมูลการคืนค่าของฟังก์ชัน

กรณีการใช้งานทั่วไปคือการอนุมานชนิดข้อมูลการคืนค่าของฟังก์ชัน:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number

function greet(name: string): string {
  return `Hello, ${name}!`;
}

type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string

ในตัวอย่างนี้ ReturnType<T> รับชนิดข้อมูลฟังก์ชัน T เป็นอินพุต จะตรวจสอบว่า T สามารถกำหนดให้กับฟังก์ชันที่รับอาร์กิวเมนต์ใดๆ และส่งคืนค่าได้หรือไม่ หากเป็นเช่นนั้น จะอนุมานชนิดข้อมูลการคืนค่าเป็น R และส่งคืน มิฉะนั้น จะส่งคืน any

2. การอนุมานชนิดข้อมูลองค์ประกอบของอาร์เรย์

อีกสถานการณ์ที่มีประโยชน์คือการดึงชนิดข้อมูลองค์ประกอบจากอาร์เรย์:

type ArrayElementType<T> = T extends (infer U)[] ? U : never;

type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never

ที่นี่ ArrayElementType<T> ตรวจสอบว่า T เป็นชนิดข้อมูลอาร์เรย์หรือไม่ หากเป็นเช่นนั้น จะอนุมานชนิดข้อมูลองค์ประกอบเป็น U และส่งคืน หากไม่ใช่ จะส่งคืน never

กรณีการใช้งานขั้นสูงของ infer

1. การอนุมานพารามิเตอร์ของคอนสตรัคเตอร์

คุณสามารถใช้ infer เพื่อดึงชนิดข้อมูลพารามิเตอร์ของฟังก์ชันคอนสตรัคเตอร์:

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]

class Point {
    constructor(public x: number, public y: number) {}
}

type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]

ในกรณีนี้ ConstructorParameters<T> รับชนิดข้อมูลฟังก์ชันคอนสตรัคเตอร์ T จะอนุมานชนิดข้อมูลของพารามิเตอร์คอนสตรัคเตอร์เป็น P และส่งคืนเป็น tuple

2. การดึงคุณสมบัติจากชนิดข้อมูลออบเจ็กต์

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

type PickByType<T, K extends keyof T, U> = {
  [P in K as T[P] extends U ? P : never]: T[P];
};

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
  isActive: boolean;
}

type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }

type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }

//An interface representing geographic coordinates.
interface GeoCoordinates {
    latitude: number;
    longitude: number;
    altitude: number;
    country: string;
    city: string;
    timezone: string;
}

type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }

ที่นี่ PickByType<T, K, U> สร้างชนิดข้อมูลใหม่ที่มีเฉพาะคุณสมบัติของ T (ที่มีคีย์ใน K) ที่ค่าสามารถกำหนดให้กับชนิดข้อมูล U ได้ ชนิดข้อมูลที่แมปจะวนซ้ำคีย์ของ T และชนิดข้อมูลแบบมีเงื่อนไขจะกรองคีย์ที่ไม่ตรงกับชนิดข้อมูลที่ระบุ

3. การทำงานกับ Promises

คุณสามารถอนุมานชนิดข้อมูลที่แก้ไขแล้วของ Promise:

type Awaited<T> = T extends Promise<infer U> ? U : T;

async function fetchData(): Promise<string> {
  return 'Data from API';
}

type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string

async function fetchNumbers(): Promise<number[]> {
    return [1, 2, 3];
}

type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]

ชนิดข้อมูล Awaited<T> รับชนิดข้อมูล T ซึ่งคาดว่าจะเป็น Promise จากนั้นชนิดข้อมูลจะอนุมานชนิดข้อมูลที่แก้ไขแล้ว U ของ Promise และส่งคืน หาก T ไม่ใช่ promise จะส่งคืน T นี่คือชนิดข้อมูลยูทิลิตี้ในตัวใน TypeScript เวอร์ชันใหม่กว่า

4. การดึงชนิดข้อมูลของอาร์เรย์ของ Promises

การรวม Awaited และการอนุมานชนิดข้อมูลอาร์เรย์ช่วยให้คุณอนุมานชนิดข้อมูลที่แก้ไขโดยอาร์เรย์ของ Promises ได้ สิ่งนี้มีประโยชน์อย่างยิ่งเมื่อจัดการกับ Promise.all

type PromiseArrayReturnType<T extends Promise<any>[]> = {
    [K in keyof T]: Awaited<T[K]>;
};


async function getUSDRate(): Promise<number> {
  return 0.0069;
}

async function getEURRate(): Promise<number> {
  return 0.0064;
}

const rates = [getUSDRate(), getEURRate()];

type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]

ตัวอย่างนี้กำหนดฟังก์ชัน asynchronous สองฟังก์ชันก่อน getUSDRate และ getEURRate ซึ่งจำลองการดึงอัตราแลกเปลี่ยน จากนั้นชนิดข้อมูลยูทิลิตี้ PromiseArrayReturnType จะดึงชนิดข้อมูลที่แก้ไขแล้วจากแต่ละ Promise ในอาร์เรย์ ส่งผลให้ชนิดข้อมูล tuple โดยที่แต่ละองค์ประกอบเป็นชนิดข้อมูล awaited ของ Promise ที่สอดคล้องกัน

ตัวอย่างที่เป็นประโยชน์ในโดเมนต่างๆ

1. แอปพลิเคชันอีคอมเมิร์ซ

พิจารณาแอปพลิเคชันอีคอมเมิร์ซที่คุณดึงรายละเอียดผลิตภัณฑ์จาก API คุณสามารถใช้ infer เพื่อดึงชนิดข้อมูลของข้อมูลผลิตภัณฑ์:

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
  category: string;
  rating: number;
  countryOfOrigin: string;
}

async function fetchProduct(productId: number): Promise<Product> {
  // Simulate API call
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        id: productId,
        name: 'Example Product',
        price: 29.99,
        description: 'A sample product',
        imageUrl: 'https://example.com/image.jpg',
        category: 'Electronics',
        rating: 4.5,
        countryOfOrigin: 'Canada'
      });
    }, 500);
  });
}


type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product

function displayProductDetails(product: ProductType) {
  console.log(`Product Name: ${product.name}`);
  console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}

fetchProduct(123).then(displayProductDetails);

ในตัวอย่างนี้ เรากำหนดอินเทอร์เฟซ Product และฟังก์ชัน fetchProduct ที่ดึงรายละเอียดผลิตภัณฑ์จาก API เราใช้ Awaited และ ReturnType เพื่อดึงชนิดข้อมูล Product จากชนิดข้อมูลการคืนค่าของฟังก์ชัน fetchProduct ทำให้เราสามารถตรวจสอบชนิดข้อมูลของฟังก์ชัน displayProductDetails ได้

2. Internationalization (i18n)

สมมติว่าคุณมีฟังก์ชันการแปลที่ส่งคืนสตริงที่แตกต่างกันตาม locale คุณสามารถใช้ infer เพื่อดึงชนิดข้อมูลการคืนค่าของฟังก์ชันนี้เพื่อความปลอดภัยของชนิดข้อมูล:

interface Translations {
  greeting: string;
  farewell: string;
  welcomeMessage: (name: string) => string;
}

const enTranslations: Translations = {
  greeting: 'Hello',
  farewell: 'Goodbye',
  welcomeMessage: (name: string) => `Welcome, ${name}!`,
};

const frTranslations: Translations = {
  greeting: 'Bonjour',
  farewell: 'Au revoir',
  welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};

function getTranslation(locale: 'en' | 'fr'): Translations {
  return locale === 'en' ? enTranslations : frTranslations;
}

type TranslationType = ReturnType<typeof getTranslation>;

function greetUser(locale: 'en' | 'fr', name: string) {
  const translations = getTranslation(locale);
  console.log(translations.welcomeMessage(name));
}

greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!

ที่นี่ TranslationType ถูกอนุมานว่าเป็นอินเทอร์เฟซ Translations เพื่อให้มั่นใจว่าฟังก์ชัน greetUser มีข้อมูลชนิดข้อมูลที่ถูกต้องสำหรับการเข้าถึงสตริงที่แปลแล้ว

3. การจัดการการตอบสนองของ API

เมื่อทำงานกับ API โครงสร้างการตอบสนองอาจซับซ้อน infer สามารถช่วยดึงชนิดข้อมูลเฉพาะจากการตอบสนองของ API ที่ซ้อนกัน:

interface ApiResponse<T> {
  status: number;
  data: T;
  message?: string;
}

interface UserData {
  id: number;
  username: string;
  email: string;
  profile: {
    firstName: string;
    lastName: string;
    country: string;
    language: string;
  }
}

async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
  // Simulate API call
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        status: 200,
        data: {
          id: userId,
          username: 'johndoe',
          email: 'john.doe@example.com',
          profile: {
            firstName: 'John',
            lastName: 'Doe',
            country: 'USA',
            language: 'en'
          }
        }
      });
    }, 500);
  });
}


type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;

type UserProfileType = UserApiResponse['data']['profile'];

function displayUserProfile(profile: UserProfileType) {
  console.log(`Name: ${profile.firstName} ${profile.lastName}`);
  console.log(`Country: ${profile.country}`);
}

fetchUser(123).then((response) => {
  if (response.status === 200) {
    displayUserProfile(response.data.profile);
  }
});

ในตัวอย่างนี้ เรากำหนดอินเทอร์เฟซ ApiResponse และอินเทอร์เฟซ UserData เราใช้ infer และการจัดทำดัชนีชนิดข้อมูลเพื่อดึง UserProfileType จากการตอบสนองของ API เพื่อให้มั่นใจว่าฟังก์ชัน displayUserProfile ได้รับชนิดข้อมูลที่ถูกต้อง

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

ข้อผิดพลาดทั่วไป

ทางเลือกอื่นแทน infer

แม้ว่า infer จะเป็นเครื่องมือที่มีประสิทธิภาพ แต่มีสถานการณ์ที่แนวทางอื่นอาจเหมาะสมกว่า:

สรุป

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