1-10 Сustom Utility Types for TypeScript Projects
Tags: typescript
Published: 2024-03-31 11:23:48

In the dynamic landscape of TypeScript development, utility types stand as base tools for crafting adaptable, clear, and robust type arrangements. This article introduces 10 widely-used utility types that tackle common coding challenges, from manipulating primitive types to fine-tuning object properties for comprehensive control over immutability and optionality.

Second part: [11-20 Сustom Utility Types for TypeScript Projects](https://dev.to/antonzo/11-20-sustom-utility-types-for-typescript-projects-2bg5)

## TOC
- [Primitive](#Primitive)
- [Falsy](#Falsy)
- [Truthy](#Truthy)
- [Nullish](#Nullish)
- [NonNullableKeys](#NonNullableKeys)
- [JSONObject](#JSONObject)
- [OptionalExceptFor](#OptionalExceptFor)
- [ReadonlyDeep](#ReadonlyDeep)
- [PartialDeep](#PartialDeep)
- [Brand](#Brand)

<a name="Primitive"></a>
## Primitive

The type `Primitive` represents the set of all basic data types in JavaScript (TypeScript). Primitive can be useful for functions or variables that may need to handle a range of simple data types.

```typescript
type Primitive = string | number | bigint | boolean | symbol | 
    null | undefined;
```

**Example**

The following example demonstrates how filters can accommodate various primitive data types effectively.

```typescript
interface Product {
  id: symbol; // Unique identifier
  name: string;
  price: number;
  available: boolean;
  totalSales: bigint; // Large number representing total sales
}

// The filter definition
interface FilterDefinition {
  key: keyof Product;
  value: Primitive;
}

// Function to filter products
function filterProducts(
  products: Product[],
  filters: FilterDefinition[]
): Product[] {
  return products.filter((product) => {
    return filters.every((filter) => {
      return product[filter.key] === filter.value;
    });
  });
}
```

<a name="Falsy"></a>
## Falsy

The type `Falsy` encompasses all possible values that JavaScript (TypeScript) considers "falsy". In JavaScript, a value is considered falsy if it translates to `false` when evaluated in a boolean context (e.g., in an `if` statement). This type is optimally designed for scenarios involving type coercion to Boolean across different primitive types. 

```typescript
type Falsy = false | "" | 0 | 0n | null | undefined;
```

**Example**
For instance, consider a form field that may accept several falsy values, including `null`, `false`, `0`, and empty string.

```typescript
// A utility function that returns a default value if the
// input is Falsy
function getDefaultIfFalsy<T>(
  value: T | Falsy, 
  defaultValue: T
): T {
  return value || defaultValue;
}

// Define form data interface
interface FormData {
  name: string;
  email: string;
  age: number;
  billingAddress: string;
  shippingAddress?: string;
  sameAsBillingAddress: boolean;
}

// Use `getDefaultIfFalsy` for `shippingAddress`
formData.shippingAddress = 
  getDefaultIfFalsy(formData.shippingAddress, "");
```

<a name="Truthy"></a>
## Truthy
This construction allows `Truthy<T>` to be used to filter out falsy values from type unions, preserving only those types that are considered truthy in JavaScript.

```typescript
type Truthy<T> = T extends Falsy ? never : T;
```

Here's how it can work in practice:

```typescript
// Result: 1 | {}
type Example = Truthy<"" | 1 | false | {} | undefined>;
```

**Example**

Utilizing the `Truthy` type, you can create a function that takes an object with optional properties as input and returns an object with only the properties that were filled out (truthy values), fully typed.

```typescript
function processInput<T extends object>(
  obj: T
): {[K in keyof T]: Truthy<T[K]>} {
  const result: Partial<{[K in keyof T]: Truthy<T[K]>}> = {};
  Object.entries(obj).forEach(([key, value]) => {
    if (value) {
      result[key as keyof T] = value as any;
    }
  });
  return result as {[K in keyof T]: Truthy<T[K]>};
}

const userInput = {
  name: "John",
  age: 0,
  email: ""
};

const processedInput = processInput(userInput);
console.log(processedInput); // Output: { name: "John" }
```

<a name="Nullish"></a>
## Nullish

The `Nullish` type indicates the absence of a value or signifies that a variable has not been initialized. Its primary purpose is to handle optional properties, variables or function parameters that may not always have a value. It allows to distinguish between missing values and values that are present, but with falsy values like `0`, `false`, or an empty string. Therefore, using `Nullish` helps enhance the reliability of the code by explicitly handling these `null` or `undefined` cases.

```typescript
type Nullish = null | undefined;
```

**Example**

In the example, `Nullish` is used to manage optional `UserInfo` properties, allowing the function `getFoodRecommendations` to default to specific values when those properties are not provided. This approach ensures the function can handle cases where the properties are either `null` or `undefined`, preventing potential errors that could arise from directly accessing unset or optional properties.

```typescript
interface UserInfo {
  name: string;
  favoriteFood?: string | Nullish;
  dietRestrictions?: string | Nullish;
}

function getFoodRecommendations(user: UserInfo) {
  const favoriteFood = user.favoriteFood ?? 'Generic';
  const dietRestriction = user.dietRestrictions ?? 'No Special Diet';

  // In a real app, there could be a complex logic to get proper food recommendations.
  // Here, just return a simple string for demonstration.
  return `Recommendations: ${favoriteFood}, ${dietRestriction}`;
}
```

<a name="NonNullableKeys"></a>
## NonNullableKeys

The `NonNullableKeys` type construction is used to filter out the keys of an object type that are associated with nullable (i.e., `null` or `undefined`) values. This utility type is particularly useful in scenarios where it's necessary to ensure the access of only those properties of an object that are guaranteed to be non-`null` and non-`undefined`. It can be applied, for example, in functions that require strict type safety and cannot operate on nullable properties without explicit checks.

```typescript
type NonNullableKeys<T> = { 
    [K in keyof T]: T[K] extends Nullish ? never : K 
}[keyof T];
```

**Example**

In the example below, we introduce a `UserProfile` interface with various properties. Using `NonNullableKeys`, we implement a `prepareProfileUpdate` function. This function filters out nullable properties from a user profile update object, ensuring that only properties with meaningful (non-`null`/`undefined`) values are included in the update payload. This approach can be especially valuable in API interactions where avoiding `null`/`undefined` data submission is desired for maintaining data integrity in backend systems.

```typescript
interface UserProfile {
  id: string;
  name: string | null;
  email?: string | null;
  bio?: string;
  lastLogin: Date | null;
}

function prepareProfileUpdate<T extends object>(
  profile: T
): Pick<T, NonNullableKeys<T>> {
  const updatePayload: Partial<T> = {};
  (Object.keys(profile) as Array<keyof T>).forEach(key => {
    const isValuePresent = profile[key] !== null &&
                           profile[key] !== undefined;
    if (isValuePresent) {
      updatePayload[key] = profile[key];
    }
  });
  return updatePayload as Pick<T, NonNullableKeys<T>>;
}

const userProfileUpdate: UserProfile = {
  id: "123",
  name: "John Doe",
  email: null,
  bio: "Software Developer",
  lastLogin: null,
};

const validProfileUpdate = prepareProfileUpdate(
  userProfileUpdate
);
// Output:
// { id: "123", name: "John Doe", bio: "Software Developer" }
console.log(validProfileUpdate);
```

<a name="JSONObject"></a>
## JSONObject

The `JSONObject` type is useful for defining the shape of objects that can be converted to or from JSON without loss or when interfacing with APIs that communicate using JSON. It employs a construction which is known as a recursive type or mutual recursion, wherein two or more types depend on each other. Recursive types are useful for describing data structures that can nest within themselves to an arbitrary dept.

```typescript
type JSONObject = { [key: string]: JSONValue };
type JSONValue = string | number | boolean | null | JSONObject | 
  JSONValue[];
```

**Example**
In this example, we define a configuration object for an application. This configuration object must be serializable to JSON, so we enforce its shape to conform to the `JSONObject` type.

```typescript
function saveConfiguration(config: JSONObject) {
  const serializedConfig = JSON.stringify(config);
  // In a real application, this string could be saved to a file,
  // sent to a server, etc.
  console.log(`Configuration saved: ${serializedConfig}`);
}

const appConfig: JSONObject = {
  user: {
    name: "John Doe",
    preferences: {
      theme: "dark",
      notifications: true,
    },
  },
  version: 1,
  debug: false,
};

saveConfiguration(appConfig);
```

<a name="OptionalExceptFor"></a>
## OptionalExceptFor

The `OptionalExceptFor` type is a utility type that takes an object type `T` and a set of keys `TRequiredKeys` from `T`, making all properties optional except for the specified keys. It's useful in scenarios where most properties of an object are optional, but a few are mandatory. This type facilitates a more flexible approach to typing objects without having to create multiple interfaces or types for variations of optional properties, especially in configurations, where only a subset of properties is required to be present.

```typescript
type OptionalExceptFor<T, TRequiredKeys extends keyof T> = 
    Partial<T> & Pick<T, TRequiredKeys>;
```

**Example**

In the following example, the `OptionalExceptFor` type is used to define an interface for user settings where only the `userId` is required, and all other properties are optional. This allows for more flexible object creation while ensuring that the `userId` property must always be provided.

```typescript
interface UserSettings {
  userId: number;
  notifications: boolean;
  theme: string;
  language: string;
}

type SettingsWithMandatoryID =
  OptionalExceptFor<UserSettings, 'userId'>;

const userSettings: SettingsWithMandatoryID = {
  userId: 123,
  // Optional: 'notifications', 'theme', 'language'
  theme: 'dark',
};

function configureSettings(
  settings: SettingsWithMandatoryID
) {
  // Configure user settings logic
}

configureSettings(userSettings);
```

<a name="ReadonlyDeep"></a>
## ReadonlyDeep

The `ReadonlyDeep` type is a utility that makes all properties of a given type `T` read-only, deeply. This means that not only are the top-level properties of the object made immutable, but all nested properties are also recursively marked as read-only. This type is particularly useful in scenarios where immutability is paramount, such as in Redux state management, where preventing unintended state mutations is crucial.

```typescript
type ReadonlyDeep<T> = {
  readonly [P in keyof T]: T[P] extends object ? 
  ReadonlyDeep<T[P]> : T[P];
};
```

**Example**
The following example ensures that neither the `Person` object itself nor any of its nested properties can be modified, thus demonstrating how the `ReadonlyDeep` type can be applied to ensure deep immutability.

```typescript
interface Address {
  street: string;
  city: string;
}

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

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

// Error: Cannot assign to 'name' because it is a read-only
// property.
person.name = "Antonio Zamay";
// Error: Cannot assign to 'city' because it is a read-only
// property.
person.address.city = "San Francisco";
```

<a name="PartialDeep"></a>
## PartialDeep

The `PartialDeep` type recursively makes all properties of an object type `T` optional, deeply. This type is particularly useful in scenarios where you're working with complex nested objects and need a way to partially update or specify them. For example, when handling state updates in large data structures without the need to specify every nested field, or when defining configurations that can override defaults at multiple levels.

```typescript
type PartialDeep<T> = {
   [P in keyof T]?: T[P] extends object ? PartialDeep<T[P]> : 
   T[P];
};
```

**Example**

In the example below, we use the `PartialDeep` type to define a function `updateUserProfile` that can accept partial updates to a user profile, including updates to nested objects such as `address` and `preferences`.

```typescript
interface UserProfile {
  username: string;
  age: number;
  address: {
    street: string;
    city: string;
  };
  preferences: {
    newsletter: boolean;
  };
}

function updateUserProfile(
  user: UserProfile,
  updates: PartialDeep<UserProfile>
): UserProfile {
  // Implementation for merging updates into user's profile
  return { ...user, ...updates };
}

const currentUser: UserProfile = {
  username: 'johndoe',
  age: 30,
  address: {
    street: '123 Elm St',
    city: 'Anytown',
  },
  preferences: {
    newsletter: true,
  },
};

const userProfileUpdates: PartialDeep<UserProfile> = {
  address: {
    city: 'New City',
  },
};

const updatedProfile = updateUserProfile(
  currentUser,
  userProfileUpdates
);
```

<a name="Brand"></a>
## Brand

The `Brand` type is a TypeScript utility that employs nominal typing for otherwise structurally identical types. TypeScript’s type system is structural, meaning that two objects are considered the same type if they have the same shape, regardless of the names or locations of their declarations. However, there are scenarios where treating two identically shaped objects as distinct types is beneficial, such as differentiating between types that are essentially the same but serve different purposes (e.g., user IDs and order IDs both being strings but representing different concepts). The `Brand` type works by intersecting a type `T` with a unique branding object, effectively differentiating otherwise identical types without changing the runtime behavior.

```typescript
type Brand<T, B> = T & { __brand: B };
```

**Example**

Imagine an application where both user IDs and order IDs are represented as strings. Without branding, these could be confused, leading to potential bugs. Using the `Brand` type, we can create two distinct types, `UserId` and `OrderId`, making it impossible to mistakenly assign one to the other.

```typescript
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function fetchUserById(id: UserId) {
  // Fetch user logic here
}

function fetchOrderByOrderId(id: OrderId) {
  // Fetch order logic here
}

// Creation of branded types
const userId: UserId = '12345' as UserId;
const orderId: OrderId = '67890' as OrderId;

// These calls are safe and clear
fetchUserById(userId); 
fetchOrderByOrderId(orderId);

// Error: Argument of type 'OrderId' is not assignable
// to parameter of type 'UserId'
fetchUserById(orderId);
```