One of TypeScript’s most compelling features is its ability to provide strong type checking and inference, helping developers catch errors at compile time rather than runtime. Among the numerous features that TypeScript brings to the table, utility types stand out as a versatile and indispensable tool in a developer’s toolkit.
Understanding Utility Types
Utility types in TypeScript are pre-defined type transformations that simplify and enhance the way we work with types. They are built-in constructs that allow developers to perform common type manipulations, making the process of defining and manipulating types more efficient and readable. These utility types are based on the concept of generics, which enables type parameters to be used in a flexible and reusable manner.
Want to learn more about Typescript? Visit our ultimate guide on Typescript which covers everything you need to know.
Using utility types, developers can transform, combine, and manipulate types without the need for writing complex type declarations from scratch. These types come in handy when dealing with various scenarios, including but not limited to:
- Working with Object Types: Utility types can be used to manipulate object types, adding or removing properties, making properties optional, or readonly, and more.
- Transforming Union and Intersection Types: Utility types help in manipulating union and intersection types to extract, transform, or merge them as needed.
- Array and Tuple Manipulations: When working with arrays and tuples, utility types allow for operations such as adding or removing elements, mapping over elements, and converting arrays to tuples and vice versa.
- Conditional Types: Conditional types leverage utility types to create complex type transformations based on conditions, enabling more dynamic type behavior.
- Mapped Types: Utility types like
Partial
,Required
, andReadonly
are examples of mapped types that transform every property of an object type.
Essential Utility Types
1. Partial<Type>
The Partial
utility type allows you to make all properties of a given type optional. This is particularly useful when you want to create a type with optional properties without manually specifying each property as optional.
type User = {
id: number;
name: string;
email: string;
};
type PartialUser = Partial<User>;
/*
Result:
PartialUser has all properties of User, but each property is optional
type PartialUser = {
id?: number | undefined;
name?: string | undefined;
email?: string | undefined;
}
*/
2. Required<Type>
In contrast to Partial
, the Required
utility type enforces all properties of a given type to be required. This is useful when you want to ensure that all properties are present in an object.
type PartialUser = {
id?: number;
name?: string;
email?: string;
};
type RequiredUser = Required<PartialUser>;
/*
Result:
RequiredUser enforces all properties to be present
type RequiredUser = {
id: number;
name: string;
email: string;
}
*/
3. Readonly<Type>
The Readonly
utility type makes all properties of a given type readonly, preventing any modifications after the object is created.
type MutableUser = {
id: number;
name: string;
};
type ImmutableUser = Readonly<MutableUser>;
/*
Result:
Properties of ImmutableUser are readonly
type ImmutableUser = {
readonly id: number;
readonly name: string;
}
*/
4. Record<Keys, Type>
The Record
utility type constructs an object type with specified keys and a given value type.
type Weekdays = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday';
type Workload = Record<Weekdays, number>;
/*
Result:
Workload has keys as weekdays and values as numbers
type Workload = {
Monday: number;
Tuesday: number;
Wednesday: number;
Thursday: number;
Friday: number;
}
*/
5. Pick<Type, Keys>
The Pick
utility type selects a subset of properties from a given type, creating a new type with only those properties.
type User = {
id: number;
name: string;
email: string;
age: number;
};
type UserSummary = Pick<User, 'id' | 'name'>;
/*
Result:
UserSummary only includes id and name properties from User
type UserSummary = {
id: number;
name: string;
}
*/
6. Omit<Type, Keys>
Conversely, the Omit
utility type creates a new type by excluding specified properties from a given type.
type User = {
id: number;
name: string;
email: string;
age: number;
};
type UserWithoutAge = Omit<User, 'age'>;
/*
Result:
UserWithoutAge has all properties of User except age
type UserWithoutAge = {
id: number;
name: string;
email: string;
}
*/
7. Exclude<Type, ExcludedUnion>
The Exclude
utility type generates a new type by excluding all types from the first type that are assignable to any type in the second type.
type AllColors = 'red' | 'blue' | 'green' | 'yellow';
type PrimaryColors = 'red' | 'blue';
type NonPrimaryColors = Exclude<AllColors, PrimaryColors>;
/*
Result:
NonPrimaryColors include 'green' and 'yellow'
type NonPrimaryColors = "green" | "yellow"
*/
//
8. Extract<Type, Union>
The Extract
utility type creates a new type by selecting all types from the first type that are assignable to any type in the second type.
type AllColors = 'red' | 'blue' | 'green' | 'yellow';
type PrimaryColors = 'red' | 'blue';
type PrimaryColorsOnly = Extract<AllColors, PrimaryColors>;
/*
Result:
PrimaryColorsOnly include 'red' and 'blue'
type PrimaryColorsOnly = "red" | "blue"
*/
9. ReturnType<Type>
The ReturnType
utility type extracts the return type of a function type.
type GreetingFn = () => string;
type Greeting = ReturnType<GreetingFn>;
// Greeting is inferred as string
10. Parameters<Type>
The Parameters
utility type extracts the parameter types from a function type as a tuple.
type AddFn = (a: number, b: number) => number;
type AddFnParams = Parameters<AddFn>;
// AddFnParams is inferred as [number, number]
Creating Custom Utility Types
While the built-in utility types provide significant convenience, developers can also create their own custom utility types tailored to specific use cases. These custom utility types can encapsulate common type manipulations, making code more readable, maintainable, and reusable.
For example, consider a scenario where you want to transform a list of strings into an object where each string is a property with boolean values indicating its presence. You can create a custom utility type to achieve this:
type StringToObject<T extends string> = {
[P in T]: boolean;
};
type Fruits = 'apple' | 'banana' | 'orange';
type FruitsAvailability = StringToObject<Fruits>;
// FruitsAvailability: { apple: boolean, banana: boolean, orange: boolean }
In this example, StringToObject
is a custom utility type that takes a union of string literals and transforms it into an object with boolean properties.
Leveraging Conditional Types
Conditional types are an advanced feature in TypeScript that allows you to create type transformations based on conditions. They are often used in utility types to achieve more complex and dynamic type behaviors.
Consider a scenario where you want to create a utility type that extracts the property names of a given type that have a specific value type. This can be achieved using a conditional type:
type PropertiesOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
type User = {
id: number;
name: string;
email: string;
age: number;
};
type StringProperties = PropertiesOfType<User, string>;
// StringProperties: 'name' | 'email'
In this example, the PropertiesOfType
utility type iterates through each property of the given type T
and checks if its value type extends the type U
. If the condition is met, the property name is included in the resulting type.
Conclusion
TypeScript utility types are an essential tool for any developer working with TypeScript. They provide a concise and powerful way to manipulate and transform types, making complex type operations much simpler. By leveraging built-in utility types and creating custom ones, developers can enhance code readability, reusability, and maintainability.