DEV Community

Cover image for 11-20 Сustom Utility Types for TypeScript Projects
Anton Zamay
Anton Zamay

Posted on • Edited on

11-20 Сustom Utility Types for TypeScript Projects

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

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]>;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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];
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]>]
    : []
    : [];
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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:

  1. Type Consistency: By using UserType, you ensure that the type is always consistent with the actual return type of the fetchUser function. If the return type of fetchUser changes, UserType will automatically reflect that change without needing manual updates.

  2. 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]>;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]>
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
  }
});
Enter fullscreen mode Exit fullscreen mode

Top comments (8)

Collapse
 
joepkockelkorn profile image
Joep Kockelkorn

Nice article, very useful types and good explanations with concrete examples!

PromiseType does however already exist natively: link

Collapse
 
antonzo profile image
Anton Zamay

Thank you very much for your comment. You are absolutely right. I would only add that:

  1. Awaited recursively unwraps nested promises. PromiseType doesn't.
  2. Awaited handles union types and resolves to the appropriate type.PromiseType doesn't.
Collapse
 
laihuynhnhatminh profile image
Nhật Minh (Himemiya Taiyou) • Edited

Just adding some small missing info:
NonNullableDeep required "strictNullChecks": true to work. Otherwise, the type checking won't throw an error.

Collapse
 
bodynar profile image
Artem

There's no definition of UnionResolver

Collapse
 
antonzo profile image
Anton Zamay

Would be a good exercise for you to provide one :D However, I edited of course. Thank you!

Collapse
 
nickhodges profile image
Nick Hodges

This is fantastic and amazing stuff. Thank you.

Collapse
 
denzie_gray profile image
Denzie Gray

Great article! The FunctionArgument type seems to be missing the type example of how its made btw

Collapse
 
antonzo profile image
Anton Zamay

Good catch, thank you! Edited.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.