11-20 TypeScript 项目的自定义实用程序类型

2025-06-08

11-20 TypeScript 项目的自定义实用程序类型

在我们探索 TypeScript 开发的第二部分中,我们将介绍另外十种自定义实用程序类型,它们可以扩展代码的功能,并提供额外的工具来更有效地管理类型。这些实用程序类型有助于保持代码库的简洁、高效和健壮。

第一部分:1-10 个 TypeScript 项目的自定义实用程序类型

目录

NonNullableDeep

NonNullableDeep类型是一种实用程序,用于从给定类型的所有属性中深度移除null。这意味着不仅对象的顶级属性不可空,而且所有嵌套属性也递归标记为不可空。此类型在确保对象的所有属性(包括深度嵌套的属性)都不是必需的场景中特别有用,例如在处理必须完全填充的数据时。undefinedTnullundefined

type NonNullableDeep<T> = {
  [P in keyof T]: NonNullable<T[P]> extends object 
    ? NonNullableDeep<NonNullable<T[P]>> 
    : NonNullable<T[P]>;
};
Enter fullscreen mode Exit fullscreen mode

例子

下面的示例演示了如何NonNullableDeep应用该类型来确保Person对象本身及其任何嵌套属性都不能nullundefined,从而确保整个对象被完全填充。

interface Address {
  street: string | null;
  city: string | null;
}

interface Person {
  name: string | null;
  age: number | null;
  address: Address | null;
}

const person: NonNullableDeep<Person> = {
  name: "Anton Zamay",
  age: 26,
  address: {
    street: "Secret Street 123",
    city: "Berlin",
  },
};

// Error: Type 'null' is not assignable to type 'string'.
person.name = null;
// Error: Type 'undefined' is not assignable to type 'number'.
person.age = undefined;
// Error: Type 'null' is not assignable to type 'Address'.
person.address = null;
// Error: Type 'null' is not assignable to type 'string'.
person.address.city = null;
Enter fullscreen mode Exit fullscreen mode

Merge

类型可用于通过组合两个对象类型和 的Merge<O1, O2>属性来创建新类型。当属性重叠时, 中的属性将覆盖 中的属性。这在需要扩展或自定义现有类型时尤其有用,可确保特定属性优先。O1O2O2O1

type Merge<O1, O2> = O2 & Omit<O1, keyof O2>;
Enter fullscreen mode Exit fullscreen mode

例子

在此示例中,我们定义了两种对象类型,分别代表默认设置和用户设置。使用这Merge两种类型,我们将这些设置组合起来,创建最终配置,其中userSettingsoverrides defaultSettings

type DefaultSettings = {
  theme: string;
  notifications: boolean;
  autoSave: boolean;
};

type UserSettings = {
  theme: string;
  notifications: string[];
  debugMode?: boolean;
};

const defaultSettings: DefaultSettings = {
  theme: "light",
  notifications: true,
  autoSave: true,
};

const userSettings: UserSettings = {
  theme: "dark",
  notifications: ["Warning 1", "Error 1", "Warning 2"],
  debugMode: true,
};

type FinalSettings = Merge<DefaultSettings, UserSettings>;

const finalSettings: FinalSettings = {
  ...defaultSettings,
  ...userSettings
};
Enter fullscreen mode Exit fullscreen mode

TupleToObject

TupleToObject类型是一种将元组类型转换为对象类型的实用程序,其中元组的元素成为对象的键,并根据这些元素在元组中的位置提取关联值。此类型在需要将元组转换为更结构化的对象形式的场景中特别有用,允许通过元素的名称而不是位置更直接地访问元素。

type TupleToObject<T extends [string, any][]> = {
    [P in T[number][0]]: Extract<T[number], [P, any]>[1];
};
Enter fullscreen mode Exit fullscreen mode

例子

设想这样一个场景:您正在使用一个将表架构信息存储为元组的数据库。每个元组包含一个字段名称及其对应的数据类型。这种格式通常用于数据库元数据 API 或架构迁移工具。元组格式紧凑且易于处理,但对于应用程序开发而言,使用对象更为方便。

type SchemaTuple = [
  ['id', 'number'],
  ['name', 'string'],
  ['email', 'string'],
  ['isActive', 'boolean']
];

const tableSchema: SchemaTuple = [
  ['id', 'number'],
  ['name', 'string'],
  ['email', 'string'],
  ['isActive', 'boolean'],
];

// Define the type of the transformed schema object
type TupleToObject<T extends [string, string | number | boolean][]> = {
  [P in T[number][0]]: Extract<
    T[number],
    [P, any]
  >[1];
};

type SchemaObject = TupleToObject<SchemaTuple>;

const schema: SchemaObject = tableSchema.reduce(
  (obj, [key, value]) => {
    obj[key] = value;
    return obj;
  },
  {} as SchemaObject
);

// Now you can use the schema object
console.log(schema.id);       // Output: number
console.log(schema.name);     // Output: string
console.log(schema.email);    // Output: string
console.log(schema.isActive); // Output: boolean
Enter fullscreen mode Exit fullscreen mode

ExclusiveTuple

ExclusiveTuple类型是一个实用程序,用于生成一个包含给定联合类型中唯一元素的元组T。此类型确保联合中的每个元素在生成的元组中仅包含一次,从而有效地将联合类型转换为包含联合元素所有可能的唯一排列的元组类型。这在需要枚举联合成员所有唯一组合的场景中尤其有用。

type ExclusiveTuple<T, U extends any[] = []> = T extends any
    ? Exclude<T, U[number]> extends infer V
    ? [V, ...ExclusiveTuple<Exclude<T, V>, [V, ...U]>]
    : []
    : [];
Enter fullscreen mode Exit fullscreen mode

例子

假设你正在开发一款旅游应用程序,该应用程序会为前往某个城市的游客生成独特的行程。该城市有三个主要景点:博物馆、公园和剧院。

type Attraction = 'Museum' | 'Park' | 'Theater';

type Itineraries = ExclusiveTuple<Attraction>;

// The Itineraries type will be equivalent to:
// type Itineraries = 
// ['Museum', 'Park', 'Theater'] | 
// ['Museum', 'Theater', 'Park'] | 
// ['Park', 'Museum', 'Theater'] | 
// ['Park', 'Theater', 'Museum'] | 
// ['Theater', 'Museum', 'Park'] | 
// ['Theater', 'Park', 'Museum'];
Enter fullscreen mode Exit fullscreen mode

PromiseType

typePromiseType是一个实用程序,用于提取给定 Promise 解析后的值的类型。这在使用异步代码时非常有用,因为它允许开发人员轻松推断结果的类型,而无需显式指定。

type PromiseType<T> = T extends Promise<infer U> ? U : never;
Enter fullscreen mode Exit fullscreen mode

此类型使用 TypeScript 的条件类型和infer关键字 来确定 a 的解析类型Promise。如果为Textends Promise<U>,则表示是解析为 类型的Ta ,并且是推断类型。如果不是,则该类型解析为PromiseUUTPromisenever

例子

以下示例演示了如何使用 PromiseType 类型从 Promise 中提取解析类型。通过使用此实用类型,您可以推断 Promise 将解析为的值的类型,这有助于在处理异步操作时进行类型检查并避免错误。

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

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

interface Post {
  id: number;
  title: string;
  content: string;
  userId: number;
}

async function fetchUser(userId: number): Promise<User> {
  return { id: userId, name: "Anton Zamay" };
}

async function fetchPostsByUser(userId: number): Promise<Post[]> {
  return [
    {
      id: 1,
      title: "Using the Singleton Pattern in React",
      content: "Content 1",
      userId
    },
    {
      id: 2,
      title: "Hoisting of Variables, Functions, Classes, Types, " + 
             "Interfaces in JavaScript/TypeScript",
      content: "Content 2",
      userId
    },
  ];
}

async function getUserWithPosts(
  userId: number
): Promise<{ user: User; posts: Post[] }> {
  const user = await fetchUser(userId);
  const posts = await fetchPostsByUser(userId);
  return { user, posts };
}

// Using PromiseType to infer the resolved types
type UserType = PromiseType<ReturnType<typeof fetchUser>>;
type PostsType = PromiseType<ReturnType<typeof fetchPostsByUser>>;
type UserWithPostsType = PromiseType<ReturnType<typeof getUserWithPosts>>;

async function exampleUsage() {
  const userWithPosts: UserWithPostsType = await getUserWithPosts(1);

  // The following will be type-checked to ensure correctness
  const userName: UserType["name"] = userWithPosts.user.name;
  const firstPostTitle: PostsType[0]["title"] = 
    userWithPosts.posts[0].title;

  console.log(userName); // Anton Zamay
  console.log(firstPostTitle); // Using the Singleton Pattern in React
}

exampleUsage();
Enter fullscreen mode Exit fullscreen mode

为什么我们需要UserType而不是仅仅使用User

UserType这是个好问题!使用 using而不是直接 using的主要原因User是为了确保能够根据异步函数的返回类型准确推断出返回类型。这种方法有几个优点:

  1. 类型一致性:通过使用UserType,您可以确保类型始终与函数的实际返回类型一致fetchUser。如果 的返回类型fetchUser发生变化,UserType将自动反映该变化,无需手动更新。

  2. 自动类型推断:处理复杂类型和嵌套的 Promise 时,手动确定并跟踪已解析的类型可能颇具挑战性。使用 PromiseType 可以让 TypeScript 为您推断这些类型,从而降低出错的风险。

OmitMethods

类型OmitMethods是一个实用程序,用于从给定类型中删除所有方法属性T。这意味着该类型的任何T函数属性都将被省略,从而生成一个仅包含非函数属性的新类型。

type OmitMethods<T> = Pick<T, { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]>;
Enter fullscreen mode Exit fullscreen mode

示例:
此类型在您想要从对象类型中排除方法的场景中特别有用,例如将对象序列化为 JSON 或通过 API 发送对象时,方法与对象无关,不应包含在内。以下示例演示了如何OmitMethods将 应用于对象类型以移除所有方法,从而确保生成的类型仅包含非函数属性。

interface User {
  id: number;
  name: string;
  age: number;
  greet(): void;
  updateAge(newAge: number): void;
}

const user: OmitMethods<User> = {
  id: 1,
  name: "Alice",
  age: 30,
  // greet and updateAge methods are omitted from this type
};

function sendUserData(userData: OmitMethods<User>) {
  // API call to send user data
  console.log("Sending user data:", JSON.stringify(userData));
}

sendUserData(user);
Enter fullscreen mode Exit fullscreen mode

FunctionArguments

类型FunctionArguments是一个实用程序,用于提取给定函数类型参数的类型T。这意味着,对于传递给它的任何函数类型,该类型都会返回一个表示函数形参类型的元组。此类型在需要捕获或操作函数参数类型的场景中特别有用,例如在高阶函数中或创建类型安全的事件处理程序时。

type FunctionArguments<T> = T extends (...args: infer A) => any 
  ? A 
  : never;
Enter fullscreen mode Exit fullscreen mode

例子

假设你有一个高阶函数包装器,它接受一个函数及其参数,然后使用这些参数调用该函数。使用 FunctionArguments,你可以确保被包装函数参数的类型安全。

function wrap<T extends (...args: any[]) => any>(fn: T, ...args: FunctionArguments<T>): ReturnType<T> {
  return fn(...args);
}

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

type AddArgs = FunctionArguments<typeof add>;
// AddArgs will be of type [number, number]

const result = wrap(add, 5, 10); // result is 15, and types are checked
Enter fullscreen mode Exit fullscreen mode

Promisify

Promisify类型是一种实用程序,它将给定类型的所有属性转换T为其各自类型的承诺。这意味着结果类型中的每个属性都将是Promise该属性原始类型的承诺。在处理异步操作时,此类型特别有用,因为您希望确保整个结构符合基于Promise- 的方法,从而更轻松地处理和管理异步数据。

type Promisify<T> = {
  [P in keyof T]: Promise<T[P]>
};
Enter fullscreen mode Exit fullscreen mode

示例:
假设有一个仪表板,用于显示用户的个人资料、近期活动和设置。这些信息可能从不同的服务获取。通过承诺单独的属性,我们确保用户数据的每个部分都可以独立获取、解析和处理,从而在处理异步操作时提供灵活性和效率。

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

interface Activity {
  lastLogin: Date;
  recentActions: string[];
}

interface Settings {
  theme: string;
  notifications: boolean;
}

interface UserData {
  profile: Profile;
  activity: Activity;
  settings: Settings;
}

// Promisify Utility Type
type Promisify<T> = {
  [P in keyof T]: Promise<T[P]>;
};

// Simulated Fetch Functions
const fetchProfile = (): Promise<Profile> =>
  Promise.resolve({ name: "Anton Zamay", age: 26, email: "antoniezamay@gmail.com" });

const fetchActivity = (): Promise<Activity> =>
  Promise.resolve({
    lastLogin: new Date(),
    recentActions: ["logged in", "viewed dashboard"],
  });

const fetchSettings = (): Promise<Settings> =>
  Promise.resolve({ theme: "dark", notifications: true });

// Fetching User Data
const fetchUserData = async (): Promise<Promisify<UserData>> => {
  return {
    profile: fetchProfile(),
    activity: fetchActivity(),
    settings: fetchSettings(),
  };
};

// Using Promisified User Data
const displayUserData = async () => {
  const user = await fetchUserData();

  // Handling promises for each property (might be in different places)
  const profile = await user.profile;
  const activity = await user.activity;
  const settings = await user.settings;

  console.log(`Name: ${profile.name}`);
  console.log(`Last Login: ${activity.lastLogin}`);
  console.log(`Theme: ${settings.theme}`);
};

displayUserData();
Enter fullscreen mode Exit fullscreen mode

ConstrainedFunction

ConstrainedFunction类型是一种实用程序,用于约束给定的函数类型 T,以确保其参数和返回类型得以保留。它本质上捕获函数的参数类型和返回类型,并强制生成的函数类型必须遵循这些推断的类型。在需要对高阶函数强制执行严格的类型约束,或创建必须符合原始函数签名的包装函数时,此类型非常有用。

type ConstrainedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R
    ? (args: A extends any[] ? A : never) => R
    : never;
Enter fullscreen mode Exit fullscreen mode

例子

在函数签名事先未知且必须动态推断的情况下,ConstrainedFunction确保根据推断的类型正确应用约束。想象一个实用程序,它包装任何函数以记忆其结果:

function memoize<T extends (...args: any) => any>(fn: T): ConstrainedFunction<T> {
  const cache = new Map<string, ReturnType<T>>();
  return ((...args: Parameters<T>) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key)!;
  }) as ConstrainedFunction<T>;
}

const greet: Greet = (name, age) => {
  return `Hello, my name is ${name} and I am ${age} years old.`;
};

const memoizedGreet = memoize(greet);

const message1 = memoizedGreet("Anton Zamay", 26); // Calculates and caches
const message2 = memoizedGreet("Anton Zamay", 26); // Retrieves from cache
Enter fullscreen mode Exit fullscreen mode

这里,memoize用于ConstrainedFunction确保记忆函数与原始函数保持相同的签名fn,而无需明确定义函数类型。

UnionResolver

UnionResolver类型是一种将联合类型转换为可区分联合的实用程序。具体来说,对于给定的联合类型T,它会生成一个对象数组,其中每个对象包含一个属性类型,该属性类型保存联合中的一种类型。在需要区分处理联合中每个成员的场景中(例如类型安全的 Redux Action 或 TypeScript 中的可区分联合模式),此类型尤其有用。

type UnionResolver<T> = T extends infer U ? { type: U }[] : never;
Enter fullscreen mode Exit fullscreen mode

示例
以下示例演示了如何UnionResolver应用该类型将联合类型转换为包含每个对象且每个对象都具有type属性的对象数组。这样可以对联合中的每个操作进行类型安全的处理,确保所有情况都得到考虑,并降低使用联合类型时出错的风险。

type ActionType = "ADD_TODO" | "REMOVE_TODO" | "UPDATE_TODO";

type ResolvedActions = UnionResolver<ActionType>;

// The resulting type will be:
// {
//   type: "ADD_TODO";
// }[] | {
//   type: "REMOVE_TODO";
// }[] | {
//   type: "UPDATE_TODO";
// }[]

const actions: ResolvedActions = [
  { type: "ADD_TODO" },
  { type: "REMOVE_TODO" },
  { type: "UPDATE_TODO" },
];

// Now you can handle each action type distinctly
actions.forEach(action => {
  switch (action.type) {
    case "ADD_TODO":
      console.log("Adding a todo");
      break;
    case "REMOVE_TODO":
      console.log("Removing a todo");
      break;
    case "UPDATE_TODO":
      console.log("Updating a todo");
      break;
  }
});
Enter fullscreen mode Exit fullscreen mode
鏂囩珷鏉ユ簮锛�https://dev.to/antonzo/11-20-sustom-utility-types-for-typescript-projects-2bg5
PREV
从头开始实现 JavaScript 概念
NEXT
软件定义汽车:潜力及控制理由