中文

一份关于 TypeScript 'infer' 关键字的综合指南,解释了如何将其与条件类型一起使用,以进行强大的类型提取和操作,包括高级用例。

掌握 TypeScript Infer:用于高级类型操作的条件类型提取

TypeScript 的类型系统非常强大,允许开发人员创建健壮且可维护的应用程序。实现这种能力的关键特性之一是与条件类型结合使用的 infer 关键字。这种组合提供了一种从复杂类型结构中提取特定类型的机制。这篇博文深入探讨了 infer 关键字,解释了它的功能并展示了高级用例。我们将探索适用于各种软件开发场景的实际示例,从 API 交互到复杂的数据结构操作。

什么是条件类型?

在我们深入研究 infer 之前,让我们快速回顾一下条件类型。TypeScript 中的条件类型允许您根据条件定义类型,类似于 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 关键字用于条件类型的 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 并将它们作为元组返回。

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. 使用 Promise

您可以推断 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。然后,该类型推断 Promise 的解析类型 U,并返回它。如果 T 不是 promise,则返回 T。这是 TypeScript 较新版本中的内置实用工具类型。

4. 提取 Promise 数组的类型

结合 Awaited 和数组类型推断,您可以推断由 Promise 数组解析的类型。这在处理 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]

此示例首先定义两个异步函数 getUSDRategetEURRate,它们模拟获取汇率。然后,PromiseArrayReturnType 实用工具类型从数组中的每个 Promise 中提取解析的类型,从而生成一个元组类型,其中每个元素是相应 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 获取产品详细信息。我们使用 AwaitedReturnTypefetchProduct 函数的返回类型中提取 Product 类型,从而允许我们对 displayProductDetails 函数进行类型检查。

2. 国际化 (i18n)

假设您有一个翻译函数,该函数根据语言环境返回不同的字符串。您可以使用 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 和类型索引从 API 响应中提取 UserProfileType,确保 displayUserProfile 函数接收正确的类型。

使用 infer 的最佳实践

常见陷阱

infer 的替代方案

虽然 infer 是一个强大的工具,但在某些情况下,替代方法可能更合适:

结论

TypeScript 中的 infer 关键字与条件类型结合使用时,可以解锁高级类型操作功能。它允许您从复杂类型结构中提取特定类型,使您能够编写更健壮、可维护且类型安全的代码。从推断函数返回类型到从对象类型中提取属性,可能性是巨大的。通过理解本指南中概述的原则和最佳实践,您可以充分利用 infer 的潜力并提高您的 TypeScript 技能。请记住记录您的类型,彻底测试它们,并在适当的时候考虑替代方法。掌握 infer 使您能够编写真正富有表现力和强大的 TypeScript 代码,最终带来更好的软件。