11-20 TypeScript 项目的自定义实用程序类型
在我们探索 TypeScript 开发的第二部分中,我们将介绍另外十种自定义实用程序类型,它们可以扩展代码的功能,并提供额外的工具来更有效地管理类型。这些实用程序类型有助于保持代码库的简洁、高效和健壮。
第一部分:1-10 个 TypeScript 项目的自定义实用程序类型
目录
NonNullableDeep
该NonNullableDeep
类型是一种实用程序,用于从给定类型的所有属性中深度移除null
和。这意味着不仅对象的顶级属性不可空,而且所有嵌套属性也递归标记为不可空。此类型在确保对象的所有属性(包括深度嵌套的属性)都不是必需的场景中特别有用,例如在处理必须完全填充的数据时。undefined
T
null
undefined
type NonNullableDeep<T> = {
[P in keyof T]: NonNullable<T[P]> extends object
? NonNullableDeep<NonNullable<T[P]>>
: NonNullable<T[P]>;
};
例子
下面的示例演示了如何NonNullableDeep
应用该类型来确保Person
对象本身及其任何嵌套属性都不能null
或undefined
,从而确保整个对象被完全填充。
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;
Merge
类型可用于通过组合两个对象类型和 的Merge<O1, O2>
属性来创建新类型。当属性重叠时, 中的属性将覆盖 中的属性。这在需要扩展或自定义现有类型时尤其有用,可确保特定属性优先。O1
O2
O2
O1
type Merge<O1, O2> = O2 & Omit<O1, keyof O2>;
例子
在此示例中,我们定义了两种对象类型,分别代表默认设置和用户设置。使用这Merge
两种类型,我们将这些设置组合起来,创建最终配置,其中userSettings
overrides 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
};
TupleToObject
该TupleToObject
类型是一种将元组类型转换为对象类型的实用程序,其中元组的元素成为对象的键,并根据这些元素在元组中的位置提取关联值。此类型在需要将元组转换为更结构化的对象形式的场景中特别有用,允许通过元素的名称而不是位置更直接地访问元素。
type TupleToObject<T extends [string, any][]> = {
[P in T[number][0]]: Extract<T[number], [P, any]>[1];
};
例子
设想这样一个场景:您正在使用一个将表架构信息存储为元组的数据库。每个元组包含一个字段名称及其对应的数据类型。这种格式通常用于数据库元数据 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
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]>]
: []
: [];
例子
假设你正在开发一款旅游应用程序,该应用程序会为前往某个城市的游客生成独特的行程。该城市有三个主要景点:博物馆、公园和剧院。
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'];
PromiseType
typePromiseType
是一个实用程序,用于提取给定 Promise 解析后的值的类型。这在使用异步代码时非常有用,因为它允许开发人员轻松推断结果的类型,而无需显式指定。
type PromiseType<T> = T extends Promise<infer U> ? U : never;
此类型使用 TypeScript 的条件类型和infer
关键字 来确定 a 的解析类型Promise
。如果为T
extends Promise<U>
,则表示是解析为 类型的T
a ,并且是推断类型。如果不是,则该类型解析为。Promise
U
U
T
Promise
never
例子
以下示例演示了如何使用 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();
为什么我们需要UserType
而不是仅仅使用User
?
UserType
这是个好问题!使用 using而不是直接 using的主要原因User
是为了确保能够根据异步函数的返回类型准确推断出返回类型。这种方法有几个优点:
-
类型一致性:通过使用
UserType
,您可以确保类型始终与函数的实际返回类型一致fetchUser
。如果 的返回类型fetchUser
发生变化,UserType
将自动反映该变化,无需手动更新。 -
自动类型推断:处理复杂类型和嵌套的 Promise 时,手动确定并跟踪已解析的类型可能颇具挑战性。使用 PromiseType 可以让 TypeScript 为您推断这些类型,从而降低出错的风险。
OmitMethods
类型OmitMethods
是一个实用程序,用于从给定类型中删除所有方法属性T
。这意味着该类型的任何T
函数属性都将被省略,从而生成一个仅包含非函数属性的新类型。
type OmitMethods<T> = Pick<T, { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]>;
示例:
此类型在您想要从对象类型中排除方法的场景中特别有用,例如将对象序列化为 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);
FunctionArguments
类型FunctionArguments
是一个实用程序,用于提取给定函数类型参数的类型T
。这意味着,对于传递给它的任何函数类型,该类型都会返回一个表示函数形参类型的元组。此类型在需要捕获或操作函数参数类型的场景中特别有用,例如在高阶函数中或创建类型安全的事件处理程序时。
type FunctionArguments<T> = T extends (...args: infer A) => any
? A
: never;
例子
假设你有一个高阶函数包装器,它接受一个函数及其参数,然后使用这些参数调用该函数。使用 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
Promisify
该Promisify
类型是一种实用程序,它将给定类型的所有属性转换T
为其各自类型的承诺。这意味着结果类型中的每个属性都将是Promise
该属性原始类型的承诺。在处理异步操作时,此类型特别有用,因为您希望确保整个结构符合基于Promise
- 的方法,从而更轻松地处理和管理异步数据。
type Promisify<T> = {
[P in keyof T]: Promise<T[P]>
};
示例:
假设有一个仪表板,用于显示用户的个人资料、近期活动和设置。这些信息可能从不同的服务获取。通过承诺单独的属性,我们确保用户数据的每个部分都可以独立获取、解析和处理,从而在处理异步操作时提供灵活性和效率。
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();
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;
例子
在函数签名事先未知且必须动态推断的情况下,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
这里,memoize
用于ConstrainedFunction
确保记忆函数与原始函数保持相同的签名fn
,而无需明确定义函数类型。
UnionResolver
该UnionResolver
类型是一种将联合类型转换为可区分联合的实用程序。具体来说,对于给定的联合类型T
,它会生成一个对象数组,其中每个对象包含一个属性类型,该属性类型保存联合中的一种类型。在需要区分处理联合中每个成员的场景中(例如类型安全的 Redux Action 或 TypeScript 中的可区分联合模式),此类型尤其有用。
type UnionResolver<T> = T extends infer U ? { type: U }[] : never;
示例
以下示例演示了如何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;
}
});