11-20 Сustom Utility Types for TypeScript Projects
Anton Zamay
Posted on June 2, 2024
In the second part of our exploration into TypeScript development, we introduce ten more custom utility types that expand the capabilities of your code, providing additional tools for managing types more effectively. These utility types help keep your codebase clean, efficient, and robust.
First part: 1-10 Сustom Utility Types for TypeScript Projects
TOC
- NonNullableDeep
- Merge
- TupleToObject
- ExclusiveTuple
- PromiseType
- OmitMethods
- FunctionArguments
- Promisify
- ConstrainedFunction
- UnionResolver
NonNullableDeep
The NonNullableDeep
type is a utility that removes null
and undefined
from all properties of a given type T
, deeply. This means that not only are the top-level properties of the object made non-nullable, but all nested properties are also recursively marked as non-nullable. This type is particularly useful in scenarios where ensuring that no properties of an object, including those deeply nested, are null
or undefined
is essential, such as when dealing with data that must be fully populated.
type NonNullableDeep<T> = {
[P in keyof T]: NonNullable<T[P]> extends object
? NonNullableDeep<NonNullable<T[P]>>
: NonNullable<T[P]>;
};
Example
The following example demonstrates how the NonNullableDeep
type can be applied to ensure that neither the Person
object itself nor any of its nested properties can be null
or undefined
, ensuring that the entire object is fully populated.
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
The Merge<O1, O2>
type is useful for creating a new type by combining the properties of two object types, O1
and O2
. When properties overlap, the properties from O2
will override those in O1
. This is particularly useful when you need to extend or customize existing types, ensuring that specific properties take precedence.
type Merge<O1, O2> = O2 & Omit<O1, keyof O2>;
Example
In this example, we define two object types representing default settings and user settings. Using the Merge
type, we combine these settings to create a final configuration, where 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
The TupleToObject
type is a utility that converts a tuple type into an object type, where the elements of the tuple become the keys of the object, and the associated values are extracted based on the position of these elements within the tuple. This type is particularly useful in scenarios where you need to transform a tuple into a more structured object form, allowing for more straightforward access to elements by their names instead of their positions.
type TupleToObject<T extends [string, any][]> = {
[P in T[number][0]]: Extract<T[number], [P, any]>[1];
};
Example
Consider a scenario where you are working with a database that stores table schema information as tuples. Each tuple contains a field name and its corresponding data type. This format is often used in database metadata APIs or schema migration tools. The tuple format is compact and easy to process, but for application development, it's more convenient to work with objects.
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
The ExclusiveTuple
type is a utility that generates a tuple containing unique elements from a given union type T
. This type ensures that each element of the union is included only once in the resulting tuple, effectively transforming a union type into a tuple type with all possible unique permutations of the union elements. This can be particularly useful in scenarios where you need to enumerate all unique combinations of a union's members.
type ExclusiveTuple<T, U extends any[] = []> = T extends any
? Exclude<T, U[number]> extends infer V
? [V, ...ExclusiveTuple<Exclude<T, V>, [V, ...U]>]
: []
: [];
Example
Consider a scenario where you are working on a feature for a travel application that generates unique itineraries for tourists visiting a city. The city offers three main attractions: a museum, a park, and a theater.
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
The PromiseType
type is a utility that extracts the type of the value that a given Promise resolves to. This is useful when working with asynchronous code, as it allows developers to easily infer the type of the result without explicitly specifying it.
type PromiseType<T> = T extends Promise<infer U> ? U : never;
This type uses TypeScript's conditional types and the infer
keyword to determine the resolved type of a Promise
. If T
extends Promise<U>
, it means that T
is a Promise
that resolves to type U
, and U
is the inferred type. If T
is not a Promise
, the type resolves to never
.
Example
The following example demonstrates how the PromiseType type can be used to extract the resolved type from a Promise. By using this utility type, you can infer the type of the value that a Promise will resolve to, which can help in type-checking and avoiding errors when handling asynchronous operations.
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();
Why do we need UserType
instead of just using User
?
That's a good question! The primary reason for using UserType
instead of directly using User
is to ensure that the type is accurately inferred from the return type of the asynchronous function. This approach has several advantages:
Type Consistency: By using
UserType
, you ensure that the type is always consistent with the actual return type of thefetchUser
function. If the return type offetchUser
changes,UserType
will automatically reflect that change without needing manual updates.Automatic Type Inference: When dealing with complex types and nested promises, it can be challenging to manually determine and keep track of the resolved types. Using PromiseType allows TypeScript to infer these types for you, reducing the risk of errors.
OmitMethods
The OmitMethods
type is a utility that removes all method properties from a given type T
. This means that any property of the type T
that is a function will be omitted, resulting in a new type that only includes the non-function properties.
type OmitMethods<T> = Pick<T, { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]>;
Example
This type is particularly useful in scenarios where you want to exclude methods from an object's type, such as when serializing an object to JSON or sending an object through an API, where methods are irrelevant and should not be included. The following example demonstrates how the OmitMethods
can be applied to an object type to remove all methods, ensuring that the resulting type only includes properties that are not functions.
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
The FunctionArguments
type is a utility that extracts the types of the arguments of a given function type T
. This means that for any function type passed to it, the type will return a tuple representing the types of the function's parameters. This type is particularly useful in scenarios where you need to capture or manipulate the argument types of a function, such as in higher-order functions or when creating type-safe event handlers.
type FunctionArguments<T> = T extends (...args: infer A) => any
? A
: never;
Example
Suppose you have a higher-order function wrap that takes a function and its arguments, and then calls the function with those arguments. Using FunctionArguments, you can ensure type safety for the wrapped function's arguments.
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
The Promisify
type is a utility that transforms all properties of a given type T
into promises of their respective types. This means that each property in the resulting type will be a Promise
of the original type of that property. This type is particularly useful when dealing with asynchronous operations where you want to ensure that the entire structure conforms to the Promise
-based approach, making it easier to handle and manage asynchronous data.
type Promisify<T> = {
[P in keyof T]: Promise<T[P]>
};
Example
Consider a dashboard that displays a user's profile, recent activity, and settings. These pieces of information might be fetched from different services. By promisifying separate properties, we ensure that each part of the user data can be fetched, resolved, and handled independently, providing flexibility and efficiency in dealing with asynchronous operations.
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
The ConstrainedFunction
type is a utility that constrains a given function type T to ensure its arguments and return type are preserved. It essentially captures the parameter types and return type of the function and enforces that the resulting function type must adhere to these inferred types. This type is useful in scenarios where you need to enforce strict type constraints on higher-order functions or when creating wrapper functions that must conform to the original function's signature.
type ConstrainedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R
? (args: A extends any[] ? A : never) => R
: never;
Example
In scenarios where the function signature is not known beforehand and must be inferred dynamically, ConstrainedFunction
ensures that the constraints are correctly applied based on the inferred types. Imagine a utility that wraps any function to memoize its results:
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
Here, memoize
uses ConstrainedFunction
to ensure that the memoized function maintains the same signature as the original function fn
, without needing to explicitly define the function type.
UnionResolver
The UnionResolver
type is a utility that transforms a union type into a discriminated union. Specifically, for a given union type T
, it produces an array of objects where each object contains a single property type that holds one of the types from the union. This type is particularly useful when working with union types in scenarios where you need to handle each member of the union distinctly, such as in type-safe Redux actions or discriminated union patterns in TypeScript.
type UnionResolver<T> = T extends infer U ? { type: U }[] : never;
Example
The following example demonstrates how the UnionResolver
type can be applied to transform a union type into an array of objects, each with a type
property. This allows for type-safe handling of each action within the union, ensuring that all cases are accounted for and reducing the risk of errors when working with union types.
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;
}
});
Posted on June 2, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024