Typescript makes JavaScript dev's life more easier than ever with type safety. Today we will discuss the most important and used utility types of typescript to make your DX more comfortable.
What are Utility Types?
Utility types in TypeScript are pre-defined generic types that enable the manipulation or creation of new types. These types are universally accessible in all TypeScript projects, eliminating the need to include any additional dependencies.
Partial<Type>
Suppose you have a profile type where all properties are defined below:
interface UserProfile {
name: string;
email: string;
address: string;
}
Now, during the registration process, you may not have the complete user profile. Using Partial<Type>
, you can make certain properties optional:
type PartialUserProfile = Partial<UserProfile>;
// Example during registration
const incompleteProfile: PartialUserProfile = { name: "John Doe" };
// Later, as more information becomes available
const completeProfile: PartialUserProfile = {
name: "John Doe",
email: "john@example.com",
address: "123 Main Street",
};
In this scenario, Partial<UserProfile>
allows you to create a user profile with missing information initially. As the user provides more details, you can update the profile without redefining the type. This flexibility makes the code more adaptable to situations where not all properties are available at once, demonstrating the practical use of the Partial<Type>
utility type.
Required<Type>
Required is the opposite of Partial. Let's consider a scenario where you are working on an e-commerce platform, and you have a product interface that includes various details such as name, price, and description. However, you want to ensure that all products have these properties defined. Here, the Required<Type>
utility type becomes valuable.
First, let's define a Product interface without using Required<Type>
:
interface Product {
name?: string;
price?: number;
description?: string;
}
Now, you realize that you always want these properties to be present for every product. You can enforce this using Required<Type>
:
type RequiredProduct = Required<Product>;
// Example with required properties
const completeProduct: RequiredProduct = {
name: "Laptop",
price: 999.99,
description: "Powerful laptop for professional use.",
};
// Error: Incomplete product will result in a compilation error
const incompleteProduct: RequiredProduct = { name: "Mouse" };
//Above line will throw error
Omit<Type, Keys>
:
Omit is used to remove some property from a defined type.
Let's look at an example where you have a rich Employee interface with lots of details, but you want to make a simpler version that doesn't include all the characteristics. This is where the utility type Omit<Type, Keys>
comes in handy.
First, let's define the complete Employee interface:
interface Employee {
id: number;
name: string;
position: string;
department: string;
salary: number;
}
Now, imagine you want to create a simpler version of the Employee type that excludes the salary property. You can achieve this using Omit<Type, Keys>
:
type EmployeeWithoutSalary = Omit<Employee, "salary">;
// Example with omitted property
const employeeWithoutSalary: EmployeeWithoutSalary = {
id: 1,
name: "Alice",
position: "Software Engineer",
department: "Engineering",
// Note: 'salary' property is excluded
};
In this scenario, you can define a type with all the attributes of an employee, without salary, by using Omit<Employee, "salary">
. Thus, this can be useful if you want to make an object lighter for specific tasks that don't require salary information. By eliminating certain attributes, the Omit utility type offers a simplified method of deriving new types.
Pick<Type, Keys>
Pick used to create a new type from the subset of other type.
Let's imagine you have a robust Movie interface with many attributes, but you only need a portion of those properties to complete a particular operation. The Pick<Type, Keys>
utility type comes in handy in this situation.
Let's first define the full Movie interface:
interface Movie {
title: string;
director: string;
genre: string;
releaseYear: number;
rating: number;
duration: number;
}
Now, imagine you are working on a feature that requires only the title and releaseYear properties. You can use Pick<Type, Keys>
to create a new type with just those properties:
type MovieTitleAndYear = Pick<Movie, "title" | "releaseYear">;
// Example using the picked properties
// Same as
type MovieTitleAndYear = {
title: string;
releaseYear: number;
};
In this scenario, Pick<Movie, "title" | "releaseYear">
creates a type that only includes the specified properties (title and releaseYear) from the original Movie interface. This allows you to work with a more focused subset of properties when needed, enhancing code clarity and preventing accidental usage of unnecessary information. The Pick
utility type is particularly helpful when you want to extract specific properties from a larger object or interface.
Readonly<Type>
Imagine you have a Person interface that displays information about certain people. You may want to make sure that the information doesn't change under specific circumstances. This is where TypeScript's Readonly<Type>
utility type comes in handy.
First, let's define the original Person interface:
interface Person {
name: string;
age: number;
address: string;
}
Now, suppose you want to create a version of the Person type where all properties are read-only. You can achieve this using the Readonly<Type>
utility type:
type ReadOnlyPerson = Readonly<Person>;
// Example using a read-only object
const immutablePerson: ReadOnlyPerson = {
name: "Jane",
age: 25,
address: "123 Main Street",
};
// Error: Attempting to modify a read-only property will result in a compilation error // immutablePerson.name = "Alice";
Awaited<Type>
Let's consider a scenario where you have an asynchronous function that simulates fetching user data from an API. You want to use the Awaited<Type>
utility type to represent the result of the asynchronous operation.
First, let's define a fictional asynchronous function:
async function fetchUserData(): Promise<{
id: number;
name: string;
email: string;
}> {
// Simulating an API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, name: "John Doe", email: "john@example.com" });
}, 2000); // Simulating a 2-second delay
});
}
Now, let's use the Awaited<Type>
utility type to represent the result of this asynchronous function:
type UserData = Awaited<ReturnType<typeof fetchUserData>>;
async function getUserInfo(): Promise<UserData> {
const userData = await fetchUserData();
// Now, userData has the type { id: number; name: string; email: string }
return userData;
}
// Example usage
getUserInfo().then((userInfo) => {
console.log(
`User ID: ${userInfo.id}, Name: ${userInfo.name}, Email: ${userInfo.email}`,
);
});
In this example, Awaited<ReturnType<typeof fetchUserData>>
is used to define the UserData
type, representing the awaited result of the fetchUserData function. This ensures that the getUserInfo
function returns a promise that resolves to the correct user data structure.
Record<Keys, Type>
Let's explore a scenario where you want to create a dictionary-like object to store information about different fruits and their prices. The Record<Keys, Type>
utility type in TypeScript can be useful in this context.
First, let's define a set of keys representing different fruits:
type Fruit = "apple" | "banana" | "orange";
Now, you want to create a dictionary-like object where the keys are fruits, and the values are their corresponding prices. You can use Record<Keys, Type>
to achieve this:
type FruitPrices = Record<Fruit, number>;
// Example dictionary of fruit prices
const fruitPrices: FruitPrices = {
apple: 1.0,
banana: 0.75,
orange: 1.5,
};
// Accessing individual prices
console.log(`Price of an apple: $${fruitPrices.apple}`);
console.log(`Price of a banana: $${fruitPrices.banana}`);
console.log(`Price of an orange: $${fruitPrices.orange}`);
In this example, Record<Fruit, number>
creates the FruitPrices type, ensuring that the dictionary object has keys corresponding to the defined Fruit type and values of type number.
NotNullable<Type>
Let's consider a scenario where you have a variable that may or may not be assigned a value, and you want to ensure that it is not null or undefined. The NonNullable<Type>
utility type in TypeScript can help you achieve this.
// Define a variable that may or may not be assigned a value
let nullableString: string | null | undefined;
// Use NonNullable<Type> to ensure the variable is not null or undefined
let nonNullableString: NonNullable<typeof nullableString>;
// Now, you can safely assign a string to the variable
nullableString = "Hello, TypeScript!";
nonNullableString = "Hello, TypeScript!"; // This assignment is now allowed
// Error: You can't assign null or undefined to nonNullableString anymore
// nonNullableString = null;
// nonNullableString = undefined;
In this case, NonNullable<typeof nullableString>
makes sure that null or undefined cannot be assigned to nonNullableString. This utility type helps improve type safety and avoid potential runtime errors related to null or undefined values by allowing you to exclude null and undefined from the available types of a variable.
Parameters<Type>
The Parameters<Type>
utility type in TypeScript allows you to obtain the parameter types of a function type. This can be useful when you want to capture and use the types of parameters in a generic or utility context.
Let's illustrate this with an example:
// Define a function type with parameters of different types
type MyFunctionType = (name: string, age: number, isAdmin: boolean) => void;
// Use Parameters<Type> to capture the parameter types
type MyFunctionParameters = Parameters<MyFunctionType>;
// Now, you can use MyFunctionParameters in a generic context
function logParameters(...params: MyFunctionParameters): void {
params.forEach((param) => console.log(param));
}
// Example usage
logParameters("John Doe", 25, false);
In this example, Parameters<MyFunctionType>
extracts the types of the parameters from MyFunctionType, resulting in the type [string, number, boolean]. This type is then used in the logParameters function, allowing it to accept arguments of the same types as the original function.
Conclusion
In conclusion, TypeScript's utility types are powerful tools that provide a wide range of functionality for manipulating and creating new types. By leveraging these utility types, you can improve the expressiveness, safety, and flexibility of your TypeScript code, leading to more robust and maintainable applications. Understanding and using these utility types effectively can significantly enhance your development experience and help you build better software.