Chapter 7: Utility Types
Authored by syscook.dev
What are Utility Types in TypeScript?
Utility types are built-in generic types that provide common type transformations. They help you manipulate existing types to create new types without writing complex type definitions from scratch.
Key Utility Types:
- Partial<T>: Makes all properties optional
- Required<T>: Makes all properties required
- Readonly<T>: Makes all properties read-only
- Pick<T, K>: Selects specific properties
- Omit<T, K>: Excludes specific properties
- Record<K, V>: Creates object type with specific keys and values
- Exclude<T, U>: Excludes types from union
- Extract<T, U>: Extracts types from union
- NonNullable<T>: Removes null and undefined
- Parameters<T>: Gets function parameter types
- ReturnType<T>: Gets function return type
- ConstructorParameters<T>: Gets constructor parameter types
- InstanceType<T>: Gets instance type of constructor
Why Use Utility Types?
1. Type Safety with Flexibility
Utility types provide type-safe transformations without losing type information.
interface User {
id: number;
name: string;
email: string;
password: string;
isActive: boolean;
}
// Without utility types - manual type definition
interface UserUpdateData {
name?: string;
email?: string;
password?: string;
isActive?: boolean;
}
// With utility types - automatic and type-safe
type UserUpdateData = Partial<Omit<User, 'id'>>;
// Usage
const updateData: UserUpdateData = {
name: "John Updated",
email: "[email protected]"
// id is excluded, other properties are optional
};
console.log("Update data:", updateData);
Output:
Update data: { name: 'John Updated', email: '[email protected]' }
2. Code Reusability
Utility types enable creating reusable type transformations.
// Reusable type transformation
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description?: string;
category?: string;
}
// Create types with specific optional/required properties
type ProductCreate = MakeOptional<Product, 'id'>;
type ProductUpdate = MakeOptional<Product, 'id' | 'name'>;
type ProductRequired = MakeRequired<Product, 'description' | 'category'>;
const newProduct: ProductCreate = {
name: "Laptop",
price: 999,
description: "Gaming laptop"
// id is optional
};
const updateProduct: ProductUpdate = {
price: 899
// id and name are optional
};
console.log("New product:", newProduct);
console.log("Update product:", updateProduct);
Output:
New product: { name: 'Laptop', price: 999, description: 'Gaming laptop' }
Update product: { price: 899 }
How to Use Utility Types?
1. Basic Utility Types
Partial, Required, and Readonly
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Partial - makes all properties optional
type PartialUser = Partial<User>;
const partialUser: PartialUser = { name: "John" }; // All properties optional
// Required - makes all properties required
type RequiredUser = Required<PartialUser>;
const requiredUser: RequiredUser = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
// Readonly - makes all properties read-only
type ReadonlyUser = Readonly<User>;
const readonlyUser: ReadonlyUser = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
// readonlyUser.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
console.log("Partial user:", partialUser);
console.log("Required user:", requiredUser);
console.log("Readonly user:", readonlyUser);
Output:
Partial user: { name: 'John' }
Required user: { id: 1, name: 'John', email: '[email protected]', isActive: true }
Readonly user: { id: 1, name: 'John', email: '[email protected]', isActive: true }
Pick and Omit
interface User {
id: number;
name: string;
email: string;
password: string;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
// Pick - select specific properties
type UserBasicInfo = Pick<User, 'id' | 'name' | 'email'>;
const basicInfo: UserBasicInfo = {
id: 1,
name: "Alice",
email: "[email protected]"
};
// Omit - exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;
const userWithoutPassword: UserWithoutPassword = {
id: 1,
name: "Alice",
email: "[email protected]",
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
};
// Combine Pick and Omit
type UserPublicInfo = Omit<Pick<User, 'id' | 'name' | 'email' | 'isActive'>, 'id'>;
const publicInfo: UserPublicInfo = {
name: "Alice",
email: "[email protected]",
isActive: true
};
console.log("Basic info:", basicInfo);
console.log("User without password:", userWithoutPassword);
console.log("Public info:", publicInfo);
Output:
Basic info: { id: 1, name: 'Alice', email: '[email protected]' }
User without password: {
id: 1,
name: 'Alice',
email: '[email protected]',
isActive: true,
createdAt: 2024-01-15T10:30:00.000Z,
updatedAt: 2024-01-15T10:30:00.000Z
}
Public info: { name: 'Alice', email: '[email protected]', isActive: true }
Record
// Record - create object type with specific keys and values
type UserRoles = Record<string, string[]>;
const userRoles: UserRoles = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
type StatusCodes = Record<number, string>;
const statusMessages: StatusCodes = {
200: "OK",
404: "Not Found",
500: "Internal Server Error"
};
type ThemeColors = Record<'primary' | 'secondary' | 'accent', string>;
const theme: ThemeColors = {
primary: "#007bff",
secondary: "#6c757d",
accent: "#28a745"
};
console.log("User roles:", userRoles);
console.log("Status messages:", statusMessages);
console.log("Theme colors:", theme);
Output:
User roles: { admin: ['read', 'write', 'delete'], user: ['read', 'write'], guest: ['read'] }
Status messages: { 200: 'OK', 404: 'Not Found', 500: 'Internal Server Error' }
Theme colors: { primary: '#007bff', secondary: '#6c757d', accent: '#28a745' }
2. Union Type Utilities
Exclude and Extract
// Exclude - remove types from union
type AllowedTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = Exclude<AllowedTypes, null | undefined>;
const primitive: PrimitiveTypes = "hello"; // Can be string, number, or boolean
// Extract - select specific types from union
type StringOrNumber = Extract<AllowedTypes, string | number>;
const stringOrNumber: StringOrNumber = 42; // Can be string or number
// NonNullable - remove null and undefined
type CleanString = NonNullable<string | null | undefined>;
const cleanString: CleanString = "world"; // Cannot be null or undefined
console.log("Primitive:", primitive);
console.log("String or number:", stringOrNumber);
console.log("Clean string:", cleanString);
Output:
Primitive: hello
String or number: 42
Clean string: world
3. Function Type Utilities
Parameters and ReturnType
// Function type utilities
function createUser(name: string, email: string, age: number): { id: number; name: string; email: string; age: number } {
return {
id: Math.random(),
name,
email,
age
};
}
// Parameters - get function parameter types
type CreateUserParams = Parameters<typeof createUser>;
const params: CreateUserParams = ["John", "[email protected]", 25];
// ReturnType - get function return type
type CreateUserReturn = ReturnType<typeof createUser>;
const user: CreateUserReturn = {
id: 1,
name: "Alice",
email: "[email protected]",
age: 30
};
// ConstructorParameters - get constructor parameter types
class User {
constructor(public name: string, public email: string, public age: number) {}
}
type UserConstructorParams = ConstructorParameters<typeof User>;
const constructorParams: UserConstructorParams = ["Bob", "[email protected]", 35];
// InstanceType - get instance type of constructor
type UserInstance = InstanceType<typeof User>;
const userInstance: UserInstance = new User("Charlie", "[email protected]", 40);
console.log("Function params:", params);
console.log("Function return:", user);
console.log("Constructor params:", constructorParams);
console.log("User instance:", userInstance);
Output:
Function params: ['John', '[email protected]', 25]
Function return: { id: 1, name: 'Alice', email: '[email protected]', age: 30 }
Constructor params: ['Bob', '[email protected]', 35]
User instance: User { name: 'Charlie', email: '[email protected]', age: 40 }
4. Advanced Utility Types
Custom Utility Types
// Deep Partial - make nested properties optional
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface NestedConfig {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
baseUrl: string;
timeout: number;
};
}
type PartialConfig = DeepPartial<NestedConfig>;
const partialConfig: PartialConfig = {
database: {
host: "localhost"
// port and credentials are optional
}
// api is optional
};
// Deep Readonly - make nested properties readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
type ReadonlyConfig = DeepReadonly<NestedConfig>;
const readonlyConfig: ReadonlyConfig = {
database: {
host: "localhost",
port: 5432,
credentials: {
username: "admin",
password: "secret"
}
},
api: {
baseUrl: "https://api.example.com",
timeout: 5000
}
};
// readonlyConfig.database.host = "new-host"; // Error: Cannot assign to 'host' because it is a read-only property
console.log("Partial config:", partialConfig);
console.log("Readonly config:", readonlyConfig);
Output:
Partial config: { database: { host: 'localhost' } }
Readonly config: {
database: {
host: 'localhost',
port: 5432,
credentials: { username: 'admin', password: 'secret' }
},
api: { baseUrl: 'https://api.example.com', timeout: 5000 }
}
Conditional Utility Types
// Conditional utility types
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface MixedObject {
id: number;
name: string;
getName(): string;
getAge(): number;
isActive: boolean;
}
type DataOnly = NonFunctionProperties<MixedObject>;
const dataOnly: DataOnly = {
id: 1,
name: "John",
isActive: true
// getName and getAge are excluded
};
// String property names only
type StringPropertyNames<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
type StringProperties<T> = Pick<T, StringPropertyNames<T>>;
type StringOnly = StringProperties<MixedObject>;
const stringOnly: StringOnly = {
name: "John"
// id and isActive are excluded
};
console.log("Data only:", dataOnly);
console.log("String only:", stringOnly);
Output:
Data only: { id: 1, name: 'John', isActive: true }
String only: { name: 'John' }
Practical Examples
1. API Response Types
// API response utility types
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface User {
id: number;
name: string;
email: string;
profile: {
avatar: string;
bio: string;
settings: {
theme: string;
notifications: boolean;
};
};
}
// Create different response types
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<User[]>;
type UserCreateResponse = ApiResponse<Pick<User, 'id' | 'name' | 'email'>>;
type UserUpdateResponse = ApiResponse<Partial<User>>;
// Error response type
type ErrorResponse = ApiResponse<{
code: string;
details: string;
}>;
// Usage
const userResponse: UserResponse = {
data: {
id: 1,
name: "John",
email: "[email protected]",
profile: {
avatar: "avatar.jpg",
bio: "Software developer",
settings: {
theme: "dark",
notifications: true
}
}
},
status: 200,
message: "Success",
timestamp: new Date().toISOString()
};
const errorResponse: ErrorResponse = {
data: {
code: "USER_NOT_FOUND",
details: "User with ID 999 does not exist"
},
status: 404,
message: "Not Found",
timestamp: new Date().toISOString()
};
console.log("User response:", userResponse);
console.log("Error response:", errorResponse);
Output:
User response: {
data: {
id: 1,
name: 'John',
email: '[email protected]',
profile: {
avatar: 'avatar.jpg',
bio: 'Software developer',
settings: { theme: 'dark', notifications: true }
}
},
status: 200,
message: 'Success',
timestamp: '2024-01-15T10:30:00.000Z'
}
Error response: {
data: { code: 'USER_NOT_FOUND', details: 'User with ID 999 does not exist' },
status: 404,
message: 'Not Found',
timestamp: '2024-01-15T10:30:00.000Z'
}
2. Form Handling Types
// Form handling with utility types
interface UserForm {
name: string;
email: string;
password: string;
confirmPassword: string;
age: number;
terms: boolean;
}
// Form field types
type FormFieldNames = keyof UserForm;
type FormFieldValues = UserForm[FormFieldNames];
// Form validation types
type RequiredFields = 'name' | 'email' | 'password';
type OptionalFields = Exclude<FormFieldNames, RequiredFields>;
type FormValidation = {
[K in FormFieldNames]: {
required: K extends RequiredFields ? true : false;
value: UserForm[K];
error?: string;
};
};
// Form state types
type FormState = {
values: UserForm;
errors: Partial<Record<FormFieldNames, string>>;
touched: Partial<Record<FormFieldNames, boolean>>;
isValid: boolean;
};
// Form submission types
type FormSubmission = Pick<UserForm, 'name' | 'email' | 'password' | 'age'>;
type FormUpdate = Partial<Omit<UserForm, 'password' | 'confirmPassword'>>;
// Usage
const formState: FormState = {
values: {
name: "John",
email: "[email protected]",
password: "secret123",
confirmPassword: "secret123",
age: 25,
terms: true
},
errors: {},
touched: {
name: true,
email: true
},
isValid: true
};
const formSubmission: FormSubmission = {
name: formState.values.name,
email: formState.values.email,
password: formState.values.password,
age: formState.values.age
};
const formUpdate: FormUpdate = {
name: "John Updated",
email: "[email protected]",
age: 26
// password and confirmPassword are excluded
};
console.log("Form state:", formState);
console.log("Form submission:", formSubmission);
console.log("Form update:", formUpdate);
Output:
Form state: {
values: {
name: 'John',
email: '[email protected]',
password: 'secret123',
confirmPassword: 'secret123',
age: 25,
terms: true
},
errors: {},
touched: { name: true, email: true },
isValid: true
}
Form submission: {
name: 'John',
email: '[email protected]',
password: 'secret123',
age: 25
}
Form update: { name: 'John Updated', email: '[email protected]', age: 26 }
Best Practices
1. Use Utility Types for Common Transformations
// Good: Use built-in utility types
type PartialUser = Partial<User>;
type RequiredUser = Required<PartialUser>;
type UserBasicInfo = Pick<User, 'id' | 'name' | 'email'>;
2. Create Custom Utility Types for Reusable Patterns
// Good: Create reusable utility types
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type NonNullable<T> = T extends null | undefined ? never : T;
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
3. Combine Utility Types for Complex Transformations
// Good: Combine utility types
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
type UserCreateRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UserCreateResponse = ApiResponse<Pick<User, 'id' | 'name' | 'email'>>;
4. Use Utility Types for API Contracts
// Good: Use utility types for API contracts
type CreateUserRequest = Omit<User, 'id'>;
type UpdateUserRequest = Partial<Omit<User, 'id'>>;
type UserResponse = Pick<User, 'id' | 'name' | 'email'>;
Common Pitfalls and Solutions
1. Deep Utility Types Performance
// ❌ Problem: Deep utility types can be slow
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// ✅ Solution: Use more specific types when possible
type ShallowPartial<T> = {
[P in keyof T]?: T[P];
};
2. Circular Type References
// ❌ Problem: Circular reference
type Circular<T> = T extends string ? Circular<T> : never;
// ✅ Solution: Use proper base cases
type SafeCircular<T> = T extends string ? string : never;
3. Complex Utility Type Combinations
// ❌ Problem: Too complex utility type
type Complex<T> = Partial<Pick<Omit<T, 'id'>, 'name' | 'email'>>;
// ✅ Solution: Break down into simpler types
type UserUpdate = Omit<User, 'id'>;
type UserUpdateFields = Pick<UserUpdate, 'name' | 'email'>;
type PartialUserUpdate = Partial<UserUpdateFields>;
Conclusion
Utility types in TypeScript provide powerful tools for type transformations and manipulations. By understanding:
- What utility types are and their categories
- Why they're essential for type safety and code reusability
- How to use built-in and create custom utility types
You can create sophisticated type systems that adapt to different use cases while maintaining type safety. Utility types enable building flexible APIs, form handling systems, and data transformations with compile-time type checking.
Next Steps
- Practice using built-in utility types
- Create custom utility types for your projects
- Explore advanced utility type patterns
- Move on to Chapter 8: Type Guards & Assertions
This tutorial is part of the TypeScript Mastery series by syscook.dev