11-20 Сustom Utility Types for TypeScript Projects
Tags: typescript
Published: 2024-06-02 18:45:21

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](https://dev.to/antonzo/10-sustom-utility-types-for-typescript-projects-48pe)

## TOC
- [NonNullableDeep](#NonNullableDeep)
- [Merge](#Merge)
- [TupleToObject](#TupleToObject)
- [ExclusiveTuple](#ExclusiveTuple)
- [PromiseType](#PromiseType)
- [OmitMethods](#OmitMethods)
- [FunctionArguments](#FunctionArguments)
- [Promisify](#Promisify)
- [ConstrainedFunction](#ConstrainedFunction)
- [UnionResolver](#UnionResolver)


<a name="NonNullableDeep"></a>
## `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.

```ts
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.

```ts
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;
```

<a name="Merge"></a>
## `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.

```ts
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`.

```ts
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
};
```

<a name="TupleToObject"></a>
## `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.

```ts
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.

```ts
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
```

<a name="ExclusiveTuple"></a>
## `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.

```ts
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. 

```ts
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'];
```


<a name="PromiseType"></a>
## `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.

```ts
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.

```ts
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:

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.

<a name="OmitMethods"></a>
## `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. 

```ts
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.

```ts
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);
```

<a name="FunctionArguments"></a>
## `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.

```ts
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.

```ts
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
```

<a name="Promisify"></a>
## `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.

```ts
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.

```ts
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();
```


<a name="ConstrainedFunction"></a>
## `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.

```ts
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:

```ts
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.

<a name="UnionResolver"></a>
## `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.


```ts
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.

```ts
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;
  }
});
```