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:
- Which Web Development Language Should You Learn in 2024?
- How To Successfully Hire An Android App Developer?
- 15 Web Development Tips To Enhance Your Skills
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