Tiếng Việt

Hướng dẫn toàn diện về từ khóa 'infer' trong TypeScript, giải thích cách sử dụng nó với kiểu có điều kiện để trích xuất và xử lý kiểu mạnh mẽ, bao gồm các trường hợp sử dụng nâng cao.

Làm chủ TypeScript Infer: Trích xuất Kiểu có Điều kiện để Xử lý Kiểu Nâng cao

Hệ thống kiểu của TypeScript vô cùng mạnh mẽ, cho phép các nhà phát triển tạo ra các ứng dụng vững chắc và dễ bảo trì. Một trong những tính năng chính tạo nên sức mạnh này là từ khóa infer được sử dụng cùng với các kiểu có điều kiện. Sự kết hợp này cung cấp một cơ chế để trích xuất các kiểu cụ thể từ các cấu trúc kiểu phức tạp. Bài viết này sẽ đi sâu vào từ khóa infer, giải thích chức năng của nó và giới thiệu các trường hợp sử dụng nâng cao. Chúng ta sẽ khám phá các ví dụ thực tế áp dụng cho các kịch bản phát triển phần mềm đa dạng, từ tương tác API đến xử lý cấu trúc dữ liệu phức tạp.

Kiểu có Điều kiện (Conditional Types) là gì?

Trước khi chúng ta đi sâu vào infer, hãy cùng xem lại nhanh về kiểu có điều kiện. Kiểu có điều kiện trong TypeScript cho phép bạn định nghĩa một kiểu dựa trên một điều kiện, tương tự như toán tử ba ngôi trong JavaScript. Cú pháp cơ bản là:

T extends U ? X : Y

Điều này có thể được hiểu là: "Nếu kiểu T có thể gán cho kiểu U, thì kiểu sẽ là X; ngược lại, kiểu sẽ là Y."

Ví dụ:

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

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

Giới thiệu từ khóa infer

Từ khóa infer được sử dụng trong mệnh đề extends của một kiểu có điều kiện để khai báo một biến kiểu có thể được suy luận từ kiểu đang được kiểm tra. Về bản chất, nó cho phép bạn "bắt" một phần của một kiểu để sử dụng sau này.

Cú pháp cơ bản:

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

Trong ví dụ này, nếu T có thể gán cho một kiểu nào đó, TypeScript sẽ cố gắng suy luận kiểu của U. Nếu suy luận thành công, kiểu sẽ là U; ngược lại, nó sẽ là never.

Các ví dụ đơn giản về infer

1. Suy luận Kiểu trả về của một Hàm

Một trường hợp sử dụng phổ biến là suy luận kiểu trả về của một hàm:

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

Trong ví dụ này, ReturnType<T> nhận một kiểu hàm T làm đầu vào. Nó kiểm tra xem T có thể gán cho một hàm chấp nhận bất kỳ đối số nào và trả về một giá trị hay không. Nếu có, nó suy luận kiểu trả về là R và trả về nó. Ngược lại, nó trả về any.

2. Suy luận Kiểu phần tử của Mảng

Một kịch bản hữu ích khác là trích xuất kiểu phần tử từ một mảng:

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

Ở đây, ArrayElementType<T> kiểm tra xem T có phải là một kiểu mảng hay không. Nếu có, nó suy luận kiểu phần tử là U và trả về nó. Nếu không, nó trả về never.

Các trường hợp sử dụng nâng cao của infer

1. Suy luận tham số của một Constructor

Bạn có thể sử dụng infer để trích xuất các kiểu tham số của một hàm khởi tạo (constructor):

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]

Trong trường hợp này, ConstructorParameters<T> nhận một kiểu hàm khởi tạo T. Nó suy luận các kiểu của các tham số hàm khởi tạo là P và trả về chúng dưới dạng một tuple.

2. Trích xuất thuộc tính từ Kiểu đối tượng

infer cũng có thể được sử dụng để trích xuất các thuộc tính cụ thể từ các kiểu đối tượng bằng cách sử dụng mapped types và conditional types:

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; }

//Một interface đại diện cho tọa độ địa lý.
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; }

Ở đây, PickByType<T, K, U> tạo ra một kiểu mới chỉ bao gồm các thuộc tính của T (với các key trong K) có giá trị có thể gán cho kiểu U. Mapped type lặp qua các key của T, và conditional type lọc ra các key không khớp với kiểu được chỉ định.

3. Làm việc với Promises

Bạn có thể suy luận kiểu được giải quyết của một 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[]

Kiểu Awaited<T> nhận một kiểu T, được mong đợi là một Promise. Kiểu này sau đó suy luận kiểu được giải quyết U của Promise, và trả về nó. Nếu T không phải là một promise, nó trả về T. Đây là một utility type được tích hợp sẵn trong các phiên bản mới hơn của TypeScript.

4. Trích xuất Kiểu của một Mảng các Promise

Việc kết hợp Awaited và suy luận kiểu mảng cho phép bạn suy luận kiểu được giải quyết bởi một mảng các Promise. Điều này đặc biệt hữu ích khi xử lý 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]

Ví dụ này đầu tiên định nghĩa hai hàm bất đồng bộ, getUSDRategetEURRate, mô phỏng việc lấy tỷ giá hối đoái. Utility type PromiseArrayReturnType sau đó trích xuất kiểu được giải quyết từ mỗi Promise trong mảng, kết quả là một kiểu tuple mà mỗi phần tử là kiểu đã được await của Promise tương ứng.

Các ví dụ thực tế trong các lĩnh vực khác nhau

1. Ứng dụng Thương mại điện tử

Hãy xem xét một ứng dụng thương mại điện tử nơi bạn lấy chi tiết sản phẩm từ một API. Bạn có thể sử dụng infer để trích xuất kiểu của dữ liệu sản phẩm:

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> {
  // Mô phỏng cuộc gọi API
  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);

Trong ví dụ này, chúng ta định nghĩa một interface Product và một hàm fetchProduct để lấy chi tiết sản phẩm từ một API. Chúng ta sử dụng AwaitedReturnType để trích xuất kiểu Product từ kiểu trả về của hàm fetchProduct, cho phép chúng ta kiểm tra kiểu cho hàm displayProductDetails.

2. Quốc tế hóa (i18n)

Giả sử bạn có một hàm dịch trả về các chuỗi khác nhau dựa trên ngôn ngữ. Bạn có thể sử dụng infer để trích xuất kiểu trả về của hàm này để đảm bảo an toàn kiểu:

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!

Ở đây, TranslationType được suy luận là interface Translations, đảm bảo rằng hàm greetUser có thông tin kiểu chính xác để truy cập các chuỗi đã dịch.

3. Xử lý Phản hồi API

Khi làm việc với các API, cấu trúc phản hồi có thể phức tạp. infer có thể giúp trích xuất các kiểu dữ liệu cụ thể từ các phản hồi API lồng nhau:

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>> {
  // Mô phỏng cuộc gọi API
  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);
  }
});

Trong ví dụ này, chúng ta định nghĩa một interface ApiResponse và một interface UserData. Chúng ta sử dụng infer và chỉ mục kiểu để trích xuất UserProfileType từ phản hồi API, đảm bảo rằng hàm displayUserProfile nhận được kiểu chính xác.

Các phương pháp tốt nhất khi sử dụng infer

Những cạm bẫy thường gặp

Các phương án thay thế cho infer

Mặc dù infer là một công cụ mạnh mẽ, có những tình huống mà các cách tiếp cận thay thế có thể phù hợp hơn:

Kết luận

Từ khóa infer trong TypeScript, khi kết hợp với các kiểu có điều kiện, mở ra các khả năng xử lý kiểu nâng cao. Nó cho phép bạn trích xuất các kiểu cụ thể từ các cấu trúc kiểu phức tạp, giúp bạn viết mã mạnh mẽ, dễ bảo trì và an toàn về kiểu hơn. Từ việc suy luận kiểu trả về của hàm đến trích xuất thuộc tính từ kiểu đối tượng, khả năng là vô tận. Bằng cách hiểu các nguyên tắc và các phương pháp tốt nhất được nêu trong hướng dẫn này, bạn có thể tận dụng infer hết tiềm năng của nó và nâng cao kỹ năng TypeScript của mình. Hãy nhớ ghi chú cho các kiểu của bạn, kiểm thử chúng kỹ lưỡng và xem xét các cách tiếp cận thay thế khi thích hợp. Làm chủ infer giúp bạn viết mã TypeScript thực sự biểu cảm và mạnh mẽ, cuối cùng dẫn đến phần mềm tốt hơn.