Mastering TypeScript Utility Types: Unleashing the Power for Better Code

Relia Software

Relia Software

Hien Le

Relia Software

development

8 TypeScript utility types are: Partial<Type>; Pick<Type, Keys>; Record<Keys, Type>; Exclude<Type, ExcludedUnion>; Omit<Type, Keys>; ReturnType<Type>; Required<Type>

Mastering 8 TypeScript Utility Types For Better Code

Table of Contents

TypeScript has gained immense popularity among developers for its ability to add static typing to JavaScript, making code more reliable and maintainable. One of the standout features that contribute to its power and versatility is the collection of built-in utility types. These utility types streamline common type manipulation tasks, making our lives as developers significantly better. In this blog post, we will explore 8 essential utility types in TypeScript, along with practical examples showcasing their benefits.

>> Read more:

Partial<Type>

The Partial utility type allows us to create a new type with all properties of the original type set as optional. This is particularly useful when your code needs to handle situations where only some properties from a type are provided.

interface User {
  id: number;
  name: string;
  email: string;
}

function updateUser(user: Partial<User>): void {
  // Update user properties safely, as all properties are optional.
}

const partialUser: Partial<User> = { name: "John Doe" };

In the real world, consider a user profile update feature in a web application. The user might only want to update specific information like their name or email address, leaving other fields unchanged. With Partial, you can conveniently handle this situation by allowing partial updates to the user object without enforcing all properties to be present.

In this scenario, without Partial, you would have to manually mark each property as optional in the function parameter.

Here's the same code without using Partial:

interface User {
  id: number;
  name: string;
  email: string;
}

function updateUser(user: { id?: number; name?: string; email?: string }): void {
  // Update user properties safely, using optional properties.
}

const partialUser = { name: "John Doe" }; // No need for Partial<User> here

// Usage
updateUser(partialUser);

In the above code, you manually define the updateUser function parameter with optional properties using the ? symbol after each property name. This achieves a similar effect to using Partial<User>, but it's less convenient and more error-prone, especially when dealing with types that have many properties. The Partial utility type simplifies the process of marking properties as optional, making the code more readable and maintainable.

Pick<Type, Keys>

With Pick, we can create a new type containing only the selected properties from the original type. This helps us extract specific properties without having to define a new interface.

Let's consider a scenario involving a clothing ecommerce website where you want to display basic information about products, such as their name and price. Instead of creating a new interface, you can use the Pick utility type to extract specific properties from a more comprehensive Product interface.

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

type ProductCard = Pick<Product, "name" | "price">;

const product: ProductCard = { name: "Cotton T-shirt", price: 19.99 };

In this example, the ProductCard type is created using Pick<Product, "name" | "price">, which extracts only the "name" and "price" properties from the Product interface. This makes it clear that we are only interested in displaying these properties in a product card.

Now, let's look at an example of achieving the same result without using Pick:

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

interface ProductCard {
  name: string;
  price: number;
}

const product: ProductCard = { name: "Cotton T-shirt", price: 19.99 };

In this example, we've manually created a new ProductCard interface with only the "name" and "price" properties. While this achieves the same result as using Pick, it requires defining a new interface, which can become cumbersome if you have many properties to extract or if you need to update the original interface later.

Using Pick is more concise and avoids redundancy, making the code easier to maintain, especially when dealing with larger and more complex interfaces.

Record<Keys, Type>

The Record utility type creates an object type with specified keys mapped to a particular value type. This simplifies the creation of dictionaries and mappings.

Let's keep considering an example involving a clothing ecommerce website where you want to track the available sizes for different clothing items. Instead of manually defining an object type to represent this mapping, you can use the Record utility type.

type ClothingItem = "shirt" | "pants" | "shoes";
type ClothingSizes = Record<ClothingItem, string[]>;

const availableSizes: ClothingSizes = {
  shirt: ["S", "M", "L"],
  pants: ["28", "30", "32"],
  shoes: ["7", "8", "9"],
};

In this example, the ClothingSizes type is created using Record<ClothingItem, string[]>, which maps each clothing item to an array of available sizes. This makes it easy to keep track of the available sizes for different types of clothing items.

Now, let's look at an example of achieving the same result without using Record:

type ClothingSizes = {
  shirt: string[];
  pants: string[];
  shoes: string[];
};

const availableSizes: ClothingSizes = {
  shirt: ["S", "M", "L"],
  pants: ["28", "30", "32"],
  shoes: ["7", "8", "9"],
};

In this example, we've manually defined the ClothingSizes type with each clothing item mapped to an array of available sizes. While this achieves the same result as using Record, it requires manually specifying the type for each property, which can become tedious and error-prone, especially when dealing with larger mappings.

Using Record provides a more concise and maintainable way to define mappings, making the code more readable and reducing the potential for mistakes.

Exclude<Type, ExcludedUnion>

Exclude is a utility type that produces a new type excluding specific types from a union.

Here's the example without using the Exclude utility type, in the context of a clothing ecommerce website where you want to categorize customers into three groups: "Regular," "Premium," and "VIP."

type CustomerType = "Regular" | "Premium" | "VIP";
type NonPremiumCustomers = Exclude<CustomerType, "Premium">;

const regularCustomer: NonPremiumCustomers = "Regular"; // Valid
const vipCustomer: NonPremiumCustomers = "VIP"; // Valid
const premiumCustomer: NonPremiumCustomers = "Premium"; // Error

In this example, the NonPremiumCustomers type is generated using Exclude<CustomerType, "Premium">, which excludes the "Premium" customer type from the union. This ensures that the type NonPremiumCustomers only includes "Regular" and "VIP" customer types.

Now, let's look at an example of achieving a similar result without using Exclude:

type CustomerType = "Regular" | "Premium" | "VIP";
type NonPremiumCustomers = "Regular" | "VIP";

const regularCustomer: NonPremiumCustomers = "Regular"; // Valid
const vipCustomer: NonPremiumCustomers = "VIP"; // Valid
const premiumCustomer: NonPremiumCustomers = "Premium"; // Error

In this example, we've manually defined the NonPremiumCustomers type as a union of "Regular" and "VIP." While this achieves a similar result to using Exclude, it requires explicitly listing the customer types you want to include, which can become tedious and error-prone as the number of customer types grows.

Using Exclude offers a more concise and dynamic way to create types that exclude specific values from a union, enhancing code readability and reducing maintenance efforts, especially in scenarios with larger and more diverse sets of customer types.

Omit<Type, Keys>

The Omit utility type allows us to create a new type by omitting specified properties from the original type.

interface User {
  id: number;
  username: string;
  email: string;
  password: string;
}

type PublicUser = Omit<User, "password">;

const publicUserInfo: PublicUser = {
  id: 1,
  username: "hienle",
  email: "hienle@example.com",
};

In this example, we are involving a clothing ecommerce website where you have a user type that includes sensitive information like passwords. You may need to create a type that represents a user without the password property. This is where the Omit utility type can be useful. The PublicUser type is created using Omit<User, "password">, which omits the "password" property from the original User type. This allows you to represent a user's public information without exposing the sensitive password.

Now, let's look at an example of achieving a similar result without using Omit:

interface PublicUser {
  id: number;
  username: string;
  email: string;
}

const publicUserInfo: PublicUser = {
  id: 1,
  username: "hienle",
  email: "hienle@example.com",
};

In this example, we've manually defined the PublicUser interface by including only the properties we need. While this achieves the same outcome as using Omit, it requires creating a new interface and manually listing the desired properties, which can become repetitive and error-prone when dealing with larger types.

Using Omit offers a cleaner and more streamlined approach to creating new types by excluding specific properties, making your code more organized, easier to maintain, and less prone to errors.

ReturnType<Type>

ReturnType extracts the return type of a function type, enabling us to access the function's return type without invoking it.

function greet(): string {
  return "Hello, TypeScript!";
}

type Greeting = ReturnType<typeof greet>; // Greeting will be string

In a localization library, you could have a function that retrieves translated strings based on a given key. By using ReturnType, you can ensure that the function returns a string type, preventing runtime errors when using the translation result.

Let's consider an example involving a higher-order function that takes a function as an argument and returns a new function that logs the input and output of the original function. In this case, the return type of the new function can be complex and might not be easily inferred by TypeScript.

function logFunctionCall<T extends (...args: any[]) => any>(
  func: T
): (...args: Parameters<T>) => ReturnType<T> {
  return (...args) => {
    const result = func(...args);
    console.log(`Function called with arguments: ${args}`);
    console.log(`Function result: ${result}`);
    return result;
  };
}

function add(a: number, b: number): number {
  return a + b;
}

const loggedAdd = logFunctionCall(add);

const result = loggedAdd(3, 5);

In this example, the logFunctionCall function takes a function func as an argument and returns a new function that wraps the original function and logs its input and output. The return type of the new function is (args: Parameters<T>) => ReturnType<T>, which can be quite complex.

While TypeScript can infer the types for the args parameter and the result variable, the inferred type of the loggedAdd function can be quite challenging to read due to the complex union types. It includes both the arguments and the return type of the wrapped function.

In scenarios like this, even though TypeScript can infer the types, explicitly annotating the types can improve code readability, making it clearer for developers to understand the types involved:

const loggedAdd: (a: number, b: number) => number = logFunctionCall(add);

const result: number = loggedAdd(3, 5);

While TypeScript can infer complex types, there might be cases where adding explicit type annotations can enhance code readability, especially when the inferred types are challenging to understand. So ReturnType will be helpful in the higher level of coding Typescript.

Required<Type>

The Required utility type generates a new type by making all properties of the original type required.

Let’s continue delving into an example centered around a clothing ecommerce website where users can have specific configuration preferences for their shopping experience. You want to create a type that represents a user's configuration and use it to create a default config for every user, ensuring that all the properties are mandatory.

interface UserConfig {
  darkMode?: boolean;
  currency?: string;
  showRecommendations?: boolean;
}

type CompleteUserConfig = Required<UserConfig>;

const defaultUserConfig: CompleteUserConfig = {
  darkMode: true,
  currency: "USD",
  showRecommendations: false,
};

In this instance, the CompleteUserConfig type is formed through Required<UserConfig>, which mandates all properties of the UserConfig interface to be present.

Now, let's see an example of achieving a similar result without using Required:

interface CompleteUserConfig {
  darkMode: boolean;
  currency: string;
  showRecommendations: boolean;
}

const defaultUserConfig: CompleteUserConfig = {
  darkMode: true,
  currency: "USD",
  showRecommendations: false,
};

In this example, we've manually established the CompleteUserConfig interface with all properties marked as mandatory. However, this method necessitates explicitly listing and designating each property as obligatory, which can lead to repetition and potential oversights.

By utilizing Required, you streamline the process of generating types with all properties required, contributing to cleaner code, consistent structure, and minimized chances of overlooking necessary properties.

Readonly<Type>

While TypeScript excels at ensuring data integrity, there are situations where you want to guarantee properties remain unchanged after initialization. This is where the Readonly<Type> utility type steps in, acting as a shield to protect your data's immutability.

Imagine a configuration object holding critical application settings. You wouldn't want these settings to be accidentally modified after they've been defined. Readonly empowers you to achieve this:

interface Config {
  apiKey: string;
  databaseUrl: string;
}

const config: Readonly<Config> = {
  apiKey: "your_api_key",
  databaseUrl: "your_database_url",
};

// Attempting to modify config.apiKey will result in a compilation error.
config.apiKey = "new_api_key";

Here, Readonly<Config> creates a new type where all properties from the original Config interface become read-only. Assigning a value to config creates an object of this new type, ensuring its properties cannot be modified after initialization. This safeguards your configuration data from unintended changes that could break your application.

Benefits of Readonly:

  • Enhanced Data Integrity: Readonly prevents accidental modifications, safeguarding critical data from unexpected changes.
  • Improved Code Reliability: By ensuring immutability, Readonly helps prevent errors caused by unintended data manipulation.
  • Clearer Code Intent: Using Readonly explicitly communicates that the data should not be changed, improving code readability and maintainability.

>> Read more:

Conclusion

TypeScript utility types offer a powerful set of tools to manipulate and transform types efficiently. Embracing these utility types in your TypeScript projects can enhance code clarity, reduce potential bugs, and make your development experience smoother. By leveraging Partial, Pick, Record, Exclude, Omit, ReturnType, Required, and Readonly you can create more robust and expressive type definitions, ultimately making your life as a TypeScript developer better. Happy typing!

>>> Follow and Contact Relia Software for more information!

  • Mobile App Development
  • Web application Development
  • development
  • coding