After understanding what TypeScript is and how to migrate your JavaScript project to it, the next step is diving into one of TypeScript’s most powerful and advanced features: Utility Types. These features allow you to manipulate, transform, or filter existing data types without having to redefine everything from scratch.
Utility types are incredibly useful for enhancing your data types’ flexibility and reusability, making your code more concise, safer, and easier to maintain. Let’s explore some of the most commonly used basic utility types.
Why Are Utility Types Important?
Imagine you have a complex data type for an object. However, in different scenarios, you might only need a subset of its properties, or perhaps all its properties should be optional, or you need to create a new object where keys and values come from existing types. Without utility types, you’d likely end up rewriting new type definitions repeatedly, which can be tedious and error-prone.
Utility types solve this problem by providing built-in functions for direct type manipulation, much like functions operate on variable values. This makes your type definitions DRY (Don’t Repeat Yourself) and much more manageable.
Essential Basic Utility Types You Must Know
Here are some utility types that will significantly help your development, complete with explanations and code examples showing correct usage and common pitfalls/limitations:

Partial
Partial<Type>
takes all properties of the given Type
and makes them optional. This means all properties of the resulting type can either be present or absent.
- Syntax:
Partial<T>
- When to Use:
- When creating a function to update an object where you don’t expect all properties to be provided, only those you want to change.
- When defining an object that only has a subset of properties from a complete type.
Example:
interface UserProfile {
id: number;
name: string;
email: string;
phone?: string; // Built-in optional property
}
// All properties in UpdateUserProfile become optional
type UpdateUserProfile = Partial<UserProfile>;
// --- Correct Usage ---
const existingUser: UserProfile = {
id: 1,
name: "John Doe",
email: "[email protected]",
};
const userUpdatePartial: UpdateUserProfile = {
name: "Jane Doe", // Only the 'name' property is provided, 'id' and 'email' are optional
phone: "123-456-7890" // 'phone' property can also be added
};
const userUpdateEmpty: UpdateUserProfile = {}; // An empty object is also valid
// --- Common Pitfalls / Limitations ---
// const invalidUserUpdate: UpdateUserProfile = {
// age: 30 // Error: Object literal may only specify known properties, and 'age' does not exist in type 'Partial<UserProfile>'.
// };
// Explanation: You cannot add properties that don't exist in the original type.
console.log("Partial Update:", userUpdatePartial);
console.log("Empty Update:", userUpdateEmpty);
Readonly
Readonly<Type>
takes all properties of the Type
and makes them read-only. This means that once an object of type Readonly<Type>
is created, its properties cannot be modified.
- Syntax:
Readonly<T>
- When to Use:
- To ensure data immutability, especially when an object is passed between functions or components and you don’t want it to be accidentally modified (e.g., application state that shouldn’t be directly altered).
- Defining configurations that cannot be changed after initialization.
Example:
interface Product {
sku: string;
name: string;
price: number;
}
// All properties in ImmutableProduct become read-only
type ImmutableProduct = Readonly<Product>;
// --- Correct Usage ---
const myProduct: ImmutableProduct = {
sku: "P001",
name: "Laptop Pro",
price: 1500
};
// We can read properties
console.log("Product Name:", myProduct.name); // Output: Laptop Pro
// --- Common Pitfalls / Limitations ---
// myProduct.price = 1600; // Error: Cannot assign to 'price' because it is a read-only property.
// myProduct.name = "Desktop Ultra"; // Error: Cannot assign to 'name' because it is a read-only property.
// Note Readonly's Limitation on Object Properties:
interface Config {
version: string;
settings: {
theme: string;
};
}
type ReadonlyConfig = Readonly<Config>;
const appConfig: ReadonlyConfig = {
version: "1.0.0",
settings: {
theme: "dark"
}
};
// appConfig.version = "1.0.1"; // Error: Cannot assign to 'version'.
// appConfig.settings.theme = "light"; // This IS ALLOWED! Readonly only applies to the first level.
// Explanation: Readonly only makes properties at the first level read-only. If a property's value is an object, that object's properties can still be modified. For deep (recursive) immutability, you'd need a custom solution or a library like 'immer'.
console.log("Theme after modification (Readonly):", appConfig.settings.theme); // Output: light

Pick
Pick<Type, Keys>
constructs a new object type by selecting specific properties from the given Type
. Keys
must be string literals (or a union of string literals) that exist in Type
.
- Syntax:
Pick<T, K extends keyof T>
- When to Use:
- When you only need a subset of properties from a larger type for a function or component.
- Creating a smaller
DTO (Data Transfer Object)
from a full database model.
Example:
interface UserDetails {
id: number;
username: string;
email: string;
isActive: boolean;
lastLogin: Date;
}
// Selects only 'id' and 'username' from UserDetails
type UserLoginInfo = Pick<UserDetails, "id" | "username">;
// --- Correct Usage ---
const loginInfo: UserLoginInfo = {
id: 101,
username: "dev_koding"
};
// --- Common Pitfalls / Limitations ---
// const invalidLoginInfo1: UserLoginInfo = {
// id: 102,
// username: "tester",
// email: "[email protected]" // Error: Object literal may only specify known properties, and 'email' does not exist in type 'UserLoginInfo'.
// };
// Explanation: The type generated by Pick will only have the properties you selected.
// const invalidLoginInfo2: UserLoginInfo = {
// id: 103 // Error: Property 'username' is missing in type '{ id: number; }' but required in type 'UserLoginInfo'.
// };
// Explanation: The properties you pick will remain required properties, unless you combine Pick with Partial.
// const pickNonExistent: Pick<UserDetails, "id" | "password">; // Error: Type '"password"' is not assignable to type '"id" | "username" | "email" | "isActive" | "lastLogin"'.
// Explanation: You can only pick properties that actually exist in the original `Type`.
console.log("User Login Info:", loginInfo);
Omit
Omit<Type, Keys>
constructs a new type by taking all properties from Type
except for the specific properties mentioned in Keys
. This is the functional opposite of Pick
.
- Syntax:
Omit<T, K extends keyof any>
- When to Use:
- When you have a large type and want to exclude certain sensitive or irrelevant properties for a given context (e.g., removing internal ID properties or password hashes when sending data to a client).
- Creating a slightly different variant of an existing type.
Example:
interface FullCustomer {
customerId: string;
name: string;
email: string;
creditCardInfo: string; // Sensitive property
shippingAddress: string;
}
// Creates CustomerView type by omitting 'creditCardInfo'
type CustomerView = Omit<FullCustomer, "creditCardInfo">;
// --- Correct Usage ---
const customerData: CustomerView = {
customerId: "CUST001",
name: "Budi Santoso",
email: "[email protected]",
shippingAddress: "Jl. Merdeka No. 10"
};
// --- Common Pitfalls / Limitations ---
// const invalidCustomerData: CustomerView = {
// customerId: "CUST002",
// name: "Ani",
// email: "[email protected]",
// creditCardInfo: "1234-5678-9012-3456", // Error: Object literal may only specify known properties, and 'creditCardInfo' does not exist in type 'CustomerView'.
// shippingAddress: "Jl. Damai No. 5"
// };
// Explanation: Properties that have been omitted cannot be present in the object created with the resulting type.
// const omitNonExistent: Omit<FullCustomer, "nonExistentProp">; // This is allowed, won't cause an error
// Explanation: If you try to omit a property that doesn't exist in the original type, TypeScript will simply ignore it, and no error will occur.
console.log("Customer Data:", customerData);
Record
Record<Keys, Type>
constructs an object type where the property keys (Keys
) are derived from the given string
, number
, or symbol
union, and the property values have the given Type
.
- Syntax:
Record<K extends keyof any, T>
- When to Use:
- When you want to create an object that acts as a dictionary or map, where you know the type of both the keys and values of all object properties.
- Creating an enumeration object with specific value types.
Example:
type ProductStatus = "inStock" | "outOfStock" | "discontinued";
type ProductCount = number;
// Creates an object where keys are ProductStatus and values are ProductCount
type InventoryStatus = Record<ProductStatus, ProductCount>;
// --- Correct Usage ---
const currentInventory: InventoryStatus = {
inStock: 500,
outOfStock: 20,
discontinued: 10
};
const emptyRecord: Record<string, any> = {}; // A more general Record example for an empty object with any string key and 'any' value
// --- Common Pitfalls / Limitations ---
// const incompleteInventory: InventoryStatus = {
// inStock: 100,
// outOfStock: 5 // Error: Property 'discontinued' is missing in type '{ inStock: number; outOfStock: number; }' but required in type 'Record<ProductStatus, ProductCount>'.
// };
// Explanation: All keys defined in 'Keys' must be present in the created object.
// const invalidKeyInventory: InventoryStatus = {
// inStock: 100,
// outOfStock: 5,
// discontinued: 10,
// pending: 5 // Error: Object literal may only specify known properties, and 'pending' does not exist in type 'Record<ProductStatus, ProductCount>'.
// };
// Explanation: Property keys must conform to the 'Keys' defined.
console.log("Current Inventory:", currentInventory);
Exclude
Exclude<UnionType, ExcludedMembers>
constructs a new type by excluding certain members from a union type.
- Syntax:
Exclude<T, U>
- When to Use:
- When you have a type that’s a combination of several other types, and you want to create a subset by removing specific members.
- Filtering a union type based on conditions.
Example:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
// Creates SafeHttpMethod type by excluding 'DELETE' and 'PATCH'
type SafeHttpMethod = Exclude<HttpMethod, "DELETE" | "PATCH">;
// --- Correct Usage ---
const method1: SafeHttpMethod = "GET"; // Valid
const method2: SafeHttpMethod = "POST"; // Valid
const method3: SafeHttpMethod = "PUT"; // Valid
const method4: SafeHttpMethod = "OPTIONS"; // Valid
// --- Common Pitfalls / Limitations ---
// const invalidMethod1: SafeHttpMethod = "DELETE"; // Error: Type '"DELETE"' is not assignable to type '"GET" | "POST" | "PUT" | "OPTIONS"'.
// const invalidMethod2: SafeHttpMethod = "PATCH"; // Error: Type '"PATCH"' is not assignable to type '"GET" | "POST" | "PUT" | "OPTIONS"'.
// Explanation: Excluded members cannot be used in the resulting type.
// const invalidMethod3: SafeHttpMethod = "HEAD"; // Error: Type '"HEAD"' is not assignable to type 'SafeHttpMethod'.
// Explanation: You can only exclude members that actually exist in the original 'UnionType'. If you try to use a value not in the original union, it will still be an error.
console.log("Safe Method 1:", method1);
console.log("Safe Method 2:", method2);
Utility types are incredibly powerful tools in TypeScript that allow you to work with data types more dynamically, efficiently, and safely. By mastering Partial
, Readonly
, Pick
, Omit
, Record
, and Exclude
, you’ll have a strong foundation for writing more expressive and robust TypeScript code. They reduce type duplication, improve readability, and help you build more resilient systems.
Keep experimenting with these utility types in your projects. You’ll find them to be invaluable assets in building complex and maintainable applications. Next, we’ll delve into more advanced utility types for even more complex type manipulations!