中文

解锁 TypeScript 工具类型的强大功能,编写更清晰、更易于维护和类型安全的代码。通过面向全球开发者的真实示例,探索实际应用。

精通 TypeScript 工具类型:面向全球开发者的实用指南

TypeScript 提供了一套强大的内置工具类型,可以显著增强代码的类型安全性、可读性和可维护性。这些工具类型本质上是预定义的类型转换,您可以将其应用于现有类型,从而避免编写重复且容易出错的代码。本指南将通过能引起全球开发者共鸣的实用示例,探讨各种工具类型。

为什么要使用工具类型?

工具类型解决了常见的类型操作场景。通过利用它们,您可以:

核心工具类型

Partial

Partial 会构造一个类型,其中 T 的所有属性都设置为可选。当您想为部分更新或配置对象创建类型时,这尤其有用。

示例:

假设您正在构建一个拥有来自不同地区客户的电子商务平台。您有一个 Customer 类型:


interface Customer {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  phoneNumber: string;
  address: {
    street: string;
    city: string;
    country: string;
    postalCode: string;
  };
  preferences?: {
    language: string;
    currency: string;
  }
}

在更新客户信息时,您可能不希望要求所有字段都必填。Partial 允许您定义一个类型,其中 Customer 的所有属性都是可选的:


type PartialCustomer = Partial<Customer>;

function updateCustomer(id: string, updates: PartialCustomer): void {
  // ... 更新指定ID客户的实现逻辑
}

updateCustomer("123", { firstName: "John", lastName: "Doe" }); // 有效
updateCustomer("456", { address: { city: "London" } }); // 有效

Readonly

Readonly 会构造一个类型,其中 T 的所有属性都设置为 readonly,从而防止初始化后被修改。这对于确保不可变性非常有价值。

示例:

考虑一个用于您的全局应用程序的配置对象:


interface AppConfig {
  apiUrl: string;
  theme: string;
  supportedLanguages: string[];
  version: string; // 添加了版本号
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  theme: "dark",
  supportedLanguages: ["en", "fr", "de", "es", "zh"],
  version: "1.0.0"
};

为了防止在初始化后意外修改配置,您可以使用 Readonly


type ReadonlyAppConfig = Readonly<AppConfig>;

const readonlyConfig: ReadonlyAppConfig = {
  apiUrl: "https://api.example.com",
  theme: "dark",
  supportedLanguages: ["en", "fr", "de", "es", "zh"],
  version: "1.0.0"
};

// readonlyConfig.apiUrl = "https://newapi.example.com"; // 错误:无法分配给 'apiUrl',因为它是只读属性。

Pick

Pick 通过从 T 中选取一组属性 K 来构造一个类型,其中 K 是您想要包含的属性名称的字符串字面量类型的联合。

示例:

假设您有一个包含各种属性的 Event 接口:


interface Event {
  id: string;
  title: string;
  description: string;
  location: string;
  startTime: Date;
  endTime: Date;
  organizer: string;
  attendees: string[];
}

如果您只需要 titlelocationstartTime 用于特定的显示组件,您可以使用 Pick


type EventSummary = Pick<Event, "title" | "location" | "startTime">;

function displayEventSummary(event: EventSummary): void {
  console.log(`Event: ${event.title} at ${event.location} on ${event.startTime}`);
}

Omit

Omit 通过从 T 中排除一组属性 K 来构造一个类型,其中 K 是您想要排除的属性名称的字符串字面量类型的联合。这与 Pick 相反。

示例:

使用相同的 Event 接口,如果您想为创建新事件创建一个类型,您可能希望排除 id 属性,该属性通常由后端生成:


type NewEvent = Omit<Event, "id">;

function createEvent(event: NewEvent): void {
  // ... 创建新事件的实现逻辑
}

Record

Record 构造一个对象类型,其属性键是 K,属性值是 TK 可以是字符串字面量类型、数字字面量类型或符号的联合。这非常适合创建字典或映射。

示例:

想象一下,您需要为应用程序的用户界面存储翻译。您可以使用 Record 来定义翻译的类型:


type Translations = Record<string, string>;

const enTranslations: Translations = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our platform!"
};

const frTranslations: Translations = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre plateforme !"
};

function translate(key: string, language: string): string {
  const translations = language === "en" ? enTranslations : frTranslations; // 简化版
  return translations[key] || key; // 如果找不到翻译,则回退到键本身
}

console.log(translate("hello", "en")); // 输出: Hello
console.log(translate("hello", "fr")); // 输出: Bonjour
console.log(translate("nonexistent", "en")); // 输出: nonexistent

Exclude

Exclude 通过从 T 中排除所有可分配给 U 的联合成员来构造一个类型。它对于从联合类型中过滤掉特定类型很有用。

示例:

您可能有一个代表不同事件类型的类型:


type EventType = "concert" | "conference" | "workshop" | "webinar";

如果您想创建一个排除 “webinar”(网络研讨会)事件的类型,您可以使用 Exclude


type PhysicalEvent = Exclude<EventType, "webinar">;

// PhysicalEvent 现在是 "concert" | "conference" | "workshop"

function attendPhysicalEvent(event: PhysicalEvent): void {
  console.log(`Attending a ${event}`);
}

// attendPhysicalEvent("webinar"); // 错误:类型 '"webinar"' 的参数不能赋给类型 '"concert" | "conference" | "workshop"' 的参数。

attendPhysicalEvent("concert"); // 有效

Extract

Extract 通过从 T 中提取所有可分配给 U 的联合成员来构造一个类型。这与 Exclude 相反。

示例:

使用相同的 EventType,您可以提取出网络研讨会事件类型:


type OnlineEvent = Extract<EventType, "webinar">;

// OnlineEvent 现在是 "webinar"

function attendOnlineEvent(event: OnlineEvent): void {
  console.log(`Attending a ${event} online`);
}

attendOnlineEvent("webinar"); // 有效
// attendOnlineEvent("concert"); // 错误:类型 '"concert"' 的参数不能赋给类型 '"webinar"' 的参数。

NonNullable

NonNullable 通过从 T 中排除 nullundefined 来构造一个类型。

示例:


type MaybeString = string | null | undefined;

type DefinitelyString = NonNullable<MaybeString>;

// DefinitelyString 现在是 string

function processString(str: DefinitelyString): void {
  console.log(str.toUpperCase());
}

// processString(null); // 错误:类型 'null' 的参数不能赋给类型 'string' 的参数。
// processString(undefined); // 错误:类型 'undefined' 的参数不能赋给类型 'string' 的参数。
processString("hello"); // 有效

ReturnType

ReturnType 构造一个由函数 T 的返回类型组成的类型。

示例:


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

type Greeting = ReturnType<typeof greet>;

// Greeting 现在是 string

const message: Greeting = greet("World");

console.log(message);

Parameters

Parameters 从函数类型 T 的参数类型构造一个元组类型。

示例:


function logEvent(eventName: string, eventData: object): void {
  console.log(`Event: ${eventName}`, eventData);
}

type LogEventParams = Parameters<typeof logEvent>;

// LogEventParams 现在是 [eventName: string, eventData: object]

const params: LogEventParams = ["user_login", { userId: "123", timestamp: Date.now() }];

logEvent(...params);

ConstructorParameters

ConstructorParameters 从构造函数类型 T 的参数类型构造一个元组或数组类型。它推断出需要传递给类构造函数的参数类型。

示例:


class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return "Hello, " + this.greeting;
  }
}


type GreeterParams = ConstructorParameters<typeof Greeter>;

// GreeterParams 现在是 [message: string]

const paramsGreeter: GreeterParams = ["World"];
const greeterInstance = new Greeter(...paramsGreeter);

console.log(greeterInstance.greet()); // 输出: Hello, World

Required

Required 构造一个类型,其中 T 的所有属性都设置为必需。它使所有可选属性变为必需。

示例:


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

type RequiredUserProfile = Required<UserProfile>;

// RequiredUserProfile 现在是 { name: string; age: number; email: string; }

const completeProfile: RequiredUserProfile = {
  name: "Alice",
  age: 30,
  email: "alice@example.com"
};

// const incompleteProfile: RequiredUserProfile = { name: "Bob" }; // 错误:属性 'age' 在类型 '{ name: string; }' 中缺失,但在类型 'Required' 中是必需的。

高级工具类型

模板字面量类型

模板字面量类型允许您通过连接现有的字符串字面量类型、数字字面量类型等来构造新的字符串字面量类型。这使得强大的基于字符串的类型操作成为可能。

示例:


type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIEndpoint = `/api/users` | `/api/products`;

type RequestURL = `${HTTPMethod} ${APIEndpoint}`;

// RequestURL 现在是 "GET /api/users" | "POST /api/users" | "PUT /api/users" | "DELETE /api/users" | "GET /api/products" | "POST /api/products" | "PUT /api/products" | "DELETE /api/products"

function makeRequest(url: RequestURL): void {
  console.log(`Making request to ${url}`);
}

makeRequest("GET /api/users"); // 有效
// makeRequest("INVALID /api/users"); // 错误

条件类型

条件类型允许您定义依赖于以类型关系表示的条件的类型。它们使用 infer 关键字来提取类型信息。

示例:


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

// 如果 T 是 Promise,则类型为 U;否则,类型为 T。

async function fetchData(): Promise<number> {
  return 42;
}


type Data = UnwrapPromise<ReturnType<typeof fetchData>>;

// Data 现在是 number

function processData(data: Data): void {
  console.log(data * 2);
}

processData(await fetchData());

实际应用与真实场景

让我们探讨一些更复杂的真实世界场景,在这些场景中工具类型大放异彩。

1. 表单处理

在处理表单时,您经常需要表示初始表单值、更新后的表单值以及最终提交的值。工具类型可以帮助您有效地管理这些不同的状态。


interface FormData {
  firstName: string;
  lastName: string;
  email: string;
  country: string; // 必填
  city?: string; // 可选
  postalCode?: string;
  newsletterSubscription?: boolean;
}

// 初始表单值(可选字段)
type InitialFormValues = Partial<FormData>;

// 更新后的表单值(某些字段可能缺失)
type UpdatedFormValues = Partial<FormData>;

// 提交时必需的字段
type RequiredForSubmission = Required<Pick<FormData, 'firstName' | 'lastName' | 'email' | 'country'>>;

// 在您的表单组件中使用这些类型
function initializeForm(initialValues: InitialFormValues): void { }
function updateForm(updates: UpdatedFormValues): void {}
function submitForm(data: RequiredForSubmission): void {}


const initialForm: InitialFormValues = { newsletterSubscription: true };

const updateFormValues: UpdatedFormValues = {
    firstName: "John",
    lastName: "Doe"
};

// const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test" }; // 错误:缺少 'country' 
const submissionData: RequiredForSubmission = { firstName: "test", lastName: "test", email: "test", country: "USA" }; // 正确


2. API 数据转换

当从 API 使用数据时,您可能需要将数据转换为适合您应用程序的不同格式。工具类型可以帮助您定义转换后数据的结构。


interface APIResponse {
  user_id: string;
  first_name: string;
  last_name: string;
  email_address: string;
  profile_picture_url: string;
  is_active: boolean;
}

// 将 API 响应转换为更易读的格式
type UserData = {
  id: string;
  fullName: string;
  email: string;
  avatar: string;
  active: boolean;
};

function transformApiResponse(response: APIResponse): UserData {
  return {
    id: response.user_id,
    fullName: `${response.first_name} ${response.last_name}`,
    email: response.email_address,
    avatar: response.profile_picture_url,
    active: response.is_active
  };
}

function fetchAndTransformData(url: string): Promise<UserData> {
    return fetch(url)
        .then(response => response.json())
        .then(data => transformApiResponse(data));
}


// 你甚至可以通过以下方式强制执行类型:

function saferTransformApiResponse(response: APIResponse): UserData {
    const {user_id, first_name, last_name, email_address, profile_picture_url, is_active} = response;
    const transformed: UserData = {
        id: user_id,
        fullName: `${first_name} ${last_name}`,
        email: email_address,
        avatar: profile_picture_url,
        active: is_active
    };

    return transformed;
}

3. 处理配置对象

配置对象在许多应用程序中很常见。工具类型可以帮助您定义配置对象的结构,并确保其被正确使用。


interface AppSettings {
  theme: "light" | "dark";
  language: string;
  notificationsEnabled: boolean;
  apiUrl?: string; // 针对不同环境的可选 API URL
  timeout?: number;  //可选
}

// 默认设置
const defaultSettings: AppSettings = {
  theme: "light",
  language: "en",
  notificationsEnabled: true
};

// 合并用户设置与默认设置的函数
function mergeSettings(userSettings: Partial<AppSettings>): AppSettings {
  return { ...defaultSettings, ...userSettings };
}

// 在您的应用程序中使用合并后的设置
const mergedSettings = mergeSettings({ theme: "dark", apiUrl: "https://customapi.example.com" });
console.log(mergedSettings);

有效使用工具类型的技巧

  • 从简单开始:PartialReadonly 等基本工具类型开始,然后再转向更复杂的类型。
  • 使用描述性名称:为您的类型别名赋予有意义的名称以提高可读性。
  • 组合工具类型:您可以组合多个工具类型来实现复杂的类型转换。
  • 利用编辑器支持:利用 TypeScript 出色的编辑器支持来探索工具类型的效果。
  • 理解底层概念:要有效使用工具类型,对 TypeScript 的类型系统有扎实的理解至关重要。

结论

TypeScript 工具类型是强大的工具,可以显著提高代码的质量和可维护性。通过理解并有效应用这些工具类型,您可以编写更清晰、类型更安全、更健壮的应用程序,以满足全球开发环境的需求。本指南全面概述了常见的工具类型和实用示例。请尝试使用它们,探索其增强 TypeScript 项目的潜力。请记住,在使用工具类型时要优先考虑可读性和清晰度,并始终努力编写易于理解和维护的代码,无论您的同事身在何处。