TypeScript Blitz - Day 5: Utility Types - Must Know
Master TypeScript's built-in utility types - Partial, Required, Pick, Omit, Record and more. Transform types efficiently in real-world scenarios
Goal
After this lesson you’ll master built-in utility types and use them to transform types efficiently in real-world scenarios.
Theory Part (20 min)
1. Partial - Make All Properties Optional
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial makes all properties optional
type PartialUser = Partial<User>;
// {
// id?: number;
// name?: string;
// email?: string;
// age?: number;
// }
// Practical use: update functions
function updateUser(id: number, updates: Partial<User>): User {
const existingUser = getUser(id);
return { ...existingUser, ...updates };
}
updateUser(1, { name: "John" }); // ✅ OK - only name
updateUser(1, { email: "john@email.com" }); // ✅ OK - only email
updateUser(1, { name: "John", age: 30 }); // ✅ OK - multiple fields
// Common use cases:
// - Update endpoints in APIs
// - Patch operations
// - Optional configuration overrides
2. Required - Make All Properties Required
interface Config {
apiUrl?: string;
timeout?: number;
retries?: number;
}
// Required makes all properties required
type RequiredConfig = Required<Config>;
// {
// apiUrl: string;
// timeout: number;
// retries: number;
// }
// Practical use: validation
function validateConfig(config: Required<Config>): boolean {
// Now all fields are guaranteed to exist
if (config.apiUrl.length === 0) return false;
if (config.timeout <= 0) return false;
if (config.retries < 0) return false;
return true;
}
// Must provide all fields
const fullConfig: Required<Config> = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
};
3. Readonly - Make All Properties Readonly
interface User {
id: number;
name: string;
email: string;
}
// Readonly makes all properties readonly
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
const user: ReadonlyUser = {
id: 1,
name: "John",
email: "john@example.com"
};
// user.name = "Jane"; // ❌ Error! Cannot assign to 'name' because it is read-only
// Practical use: immutable data
function processUser(user: Readonly<User>): void {
// Can read but not modify
console.log(user.name);
// user.name = "Changed"; // ❌ Error!
}
// Deep readonly (nested objects)
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
4. Pick<T, K> - Select Specific Properties
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// Pick only specific properties
type UserPreview = Pick<User, "id" | "name" | "email">;
// {
// id: number;
// name: string;
// email: string;
// }
// Practical use: API responses
function getUserPreview(id: number): UserPreview {
const fullUser = getFullUser(id);
// Return only safe fields (no password!)
return {
id: fullUser.id,
name: fullUser.name,
email: fullUser.email
};
}
// Pick for form data
type UserFormData = Pick<User, "name" | "email">;
const formData: UserFormData = {
name: "John",
email: "john@example.com"
// No need for id, password, timestamps
};
5. Omit<T, K> - Exclude Specific Properties
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Omit specific properties
type UserDTO = Omit<User, "password">;
// {
// id: number;
// name: string;
// email: string;
// createdAt: Date;
// }
// Practical use: public API responses
function getUserPublic(id: number): UserDTO {
const user = getUser(id);
// Remove sensitive data
const { password, ...publicData } = user;
return publicData;
}
// Omit multiple fields
type UserCreateInput = Omit<User, "id" | "createdAt">;
// {
// name: string;
// email: string;
// password: string;
// }
function createUser(data: UserCreateInput): User {
return {
id: generateId(),
createdAt: new Date(),
...data
};
}
6. Record<K, T> - Create Object Type with Specific Keys
// Record creates an object type with keys K and values T
type Role = "admin" | "user" | "guest";
// Map roles to permissions
type Permissions = Record<Role, string[]>;
// {
// admin: string[];
// user: string[];
// guest: string[];
// }
const permissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
// Practical use: dictionaries/maps
type UserCache = Record<string, User>;
const cache: UserCache = {
"user1": { id: 1, name: "John", email: "john@example.com" },
"user2": { id: 2, name: "Jane", email: "jane@example.com" }
};
// Record with number keys
type ScoreBoard = Record<number, number>;
const scores: ScoreBoard = {
1: 100,
2: 95,
3: 87
};
// Common use cases:
// - Configuration objects with known keys
// - Maps/dictionaries
// - Translation files
// - State management
type TranslationKeys = "welcome" | "goodbye" | "error";
type Translations = Record<TranslationKeys, string>;
const en: Translations = {
welcome: "Welcome",
goodbye: "Goodbye",
error: "Error occurred"
};
7. Exclude<T, U> - Remove Types from Union
// Exclude removes types from a union
type Status = "idle" | "loading" | "success" | "error";
// Remove specific types
type ActiveStatus = Exclude<Status, "idle">;
// "loading" | "success" | "error"
type PositiveStatus = Exclude<Status, "error" | "idle">;
// "loading" | "success"
// Practical use: filtering allowed values
type AllMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type SafeMethods = Exclude<AllMethods, "DELETE">;
// "GET" | "POST" | "PUT" | "PATCH"
function makeRequest(method: SafeMethods, url: string) {
// Can't use DELETE here
}
// Exclude with type predicates
type Primitive = string | number | boolean | null | undefined;
type NonNullable<T> = Exclude<T, null | undefined>;
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
8. Extract<T, U> - Keep Only Specific Types from Union
// Extract keeps only specified types from union
type Status = "idle" | "loading" | "success" | "error";
// Keep only specific types
type ErrorStatus = Extract<Status, "error">;
// "error"
type LoadingStates = Extract<Status, "idle" | "loading">;
// "idle" | "loading"
// Practical use: filtering types
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
// Extract only shapes with "circle" or "square"
type SimpleShape = Extract<Shape, { kind: "circle" } | { kind: "square" }>;
// Extract function types from union
type Mixed = string | number | (() => void) | boolean;
type Functions = Extract<Mixed, Function>;
// () => void
9. NonNullable - Remove null and undefined
// NonNullable removes null and undefined from type
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// User
// Practical use: after validation
function processUser(user: User | null): void {
if (user === null) {
return;
}
// user is now NonNullable<User | null> = User
console.log(user.name);
}
// Array filtering
const users: (User | null)[] = [
{ id: 1, name: "John" },
null,
{ id: 2, name: "Jane" },
null
];
// Filter out nulls with type narrowing
const validUsers: User[] = users.filter((u): u is NonNullable<typeof u> => u !== null);
10. ReturnType and Parameters - Extract from Functions
// ReturnType extracts return type
function getUser() {
return {
id: 1,
name: "John",
email: "john@example.com"
};
}
type User = ReturnType<typeof getUser>;
// {
// id: number;
// name: string;
// email: string;
// }
// Parameters extracts parameter types as tuple
function createPost(title: string, content: string, published: boolean) {
return { title, content, published };
}
type CreatePostParams = Parameters<typeof createPost>;
// [string, string, boolean]
// Practical use: wrapper functions
function loggedCreatePost(...args: Parameters<typeof createPost>): ReturnType<typeof createPost> {
console.log("Creating post with:", args);
return createPost(...args);
}
// ConstructorParameters for classes
class User {
constructor(public name: string, public age: number) {}
}
type UserConstructorParams = ConstructorParameters<typeof User>;
// [string, number]
// InstanceType - get instance type from constructor
type UserInstance = InstanceType<typeof User>;
// User
11. Awaited - Unwrap Promise Types
// Awaited unwraps Promise types
type PromisedString = Promise<string>;
type UnwrappedString = Awaited<PromisedString>;
// string
type NestedPromise = Promise<Promise<number>>;
type UnwrappedNumber = Awaited<NestedPromise>;
// number
// Practical use: async function return types
async function fetchUser(): Promise<User> {
const response = await fetch("/api/user");
return response.json();
}
type UserData = Awaited<ReturnType<typeof fetchUser>>;
// User
// Complex example
type AsyncResponse = Promise<{ data: User[] }>;
type ResponseData = Awaited<AsyncResponse>;
// { data: User[] }
// Deeply nested
type VeryNested = Promise<Promise<Promise<string>>>;
type Unwrapped = Awaited<VeryNested>;
// string
12. Combining Utility Types
// Utility types can be combined for powerful transformations
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// Example 1: Update DTO - can update some fields, but not id/timestamps
type UserUpdateDTO = Partial<Omit<User, "id" | "createdAt" | "updatedAt">>;
// {
// name?: string;
// email?: string;
// password?: string;
// }
// Example 2: Public profile - required fields, no password
type PublicProfile = Required<Omit<User, "password">>;
// {
// id: number;
// name: string;
// email: string;
// createdAt: Date;
// updatedAt: Date;
// }
// Example 3: Read-only subset
type UserPreview = Readonly<Pick<User, "id" | "name" | "email">>;
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// }
// Example 4: Create input with defaults
type UserCreateInput = Required<Omit<User, "id" | "createdAt" | "updatedAt">> & {
termsAccepted: boolean;
};
// Example 5: Map of partial entities
type UserCache = Record<string, Partial<User>>;
const cache: UserCache = {
"user1": { id: 1, name: "John" },
"user2": { email: "jane@example.com" }
};
Practice Part (35 min)
Exercise 1: User Management System (15 min)
Create type-safe CRUD operations using utility types.
// Base User interface
interface User {
id: string;
username: string;
email: string;
password: string;
role: "admin" | "user" | "guest";
createdAt: Date;
updatedAt: Date;
lastLogin?: Date;
}
// TODO: Create utility types for different operations
// 1. UserCreateInput - for creating users
// Should have: username, email, password, role
// Should NOT have: id, createdAt, updatedAt, lastLogin
type UserCreateInput = /* TODO */;
// 2. UserUpdateInput - for updating users
// All fields optional except id
// Should NOT have: id, createdAt, updatedAt
type UserUpdateInput = /* TODO */;
// 3. UserPublicProfile - safe for public display
// Should have: id, username, role, createdAt
// Should NOT have: password, email, updatedAt, lastLogin
type UserPublicProfile = /* TODO */;
// 4. UserSession - minimal data for session
// Should have: id, username, role
type UserSession = /* TODO */;
// TODO: Implement CRUD functions using these types
function createUser(input: UserCreateInput): User {
// TODO: Implement
}
function updateUser(id: string, input: UserUpdateInput): User {
// TODO: Implement
}
function getUserProfile(id: string): UserPublicProfile {
// TODO: Implement
}
function createSession(user: User): UserSession {
// TODO: Implement
}
// Tests - uncomment after implementation
// const newUser = createUser({
// username: "john_doe",
// email: "john@example.com",
// password: "secret123",
// role: "user"
// });
// const updated = updateUser("user-123", {
// username: "john_updated"
// });
// const profile = getUserProfile("user-123");
// console.log(profile.username); // ✅ OK
// console.log(profile.password); // ❌ Should error - no password in public profile
Solution:
Click to see solution
interface User {
id: string;
username: string;
email: string;
password: string;
role: "admin" | "user" | "guest";
createdAt: Date;
updatedAt: Date;
lastLogin?: Date;
}
// 1. Create input - omit auto-generated fields
type UserCreateInput = Omit<User, "id" | "createdAt" | "updatedAt" | "lastLogin">;
// 2. Update input - partial of fields that can be updated
type UserUpdateInput = Partial<Omit<User, "id" | "createdAt" | "updatedAt">>;
// 3. Public profile - pick only safe fields
type UserPublicProfile = Pick<User, "id" | "username" | "role" | "createdAt">;
// 4. Session - minimal required data
type UserSession = Pick<User, "id" | "username" | "role">;
// Implementation
function createUser(input: UserCreateInput): User {
return {
id: crypto.randomUUID(),
...input,
createdAt: new Date(),
updatedAt: new Date()
};
}
function updateUser(id: string, input: UserUpdateInput): User {
const existingUser = getUserById(id); // Mock function
return {
...existingUser,
...input,
updatedAt: new Date()
};
}
function getUserProfile(id: string): UserPublicProfile {
const user = getUserById(id); // Mock function
return {
id: user.id,
username: user.username,
role: user.role,
createdAt: user.createdAt
};
}
function createSession(user: User): UserSession {
return {
id: user.id,
username: user.username,
role: user.role
};
}
// Helper mock function
function getUserById(id: string): User {
return {
id,
username: "john_doe",
email: "john@example.com",
password: "hashed_password",
role: "user",
createdAt: new Date(),
updatedAt: new Date()
};
}
Exercise 2: API Response Transformations (12 min)
Use utility types to transform API responses.
// Raw API response
interface ApiUser {
id: number;
first_name: string;
last_name: string;
email_address: string;
phone_number: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
// TODO: Create transformed types
// 1. User - camelCase version of ApiUser
// Transform snake_case to camelCase
// Transform created_at/updated_at from string to Date
interface User {
id: number;
firstName: string;
lastName: string;
emailAddress: string;
phoneNumber: string | null;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
// 2. UserListItem - for displaying in lists
// Should have: id, firstName, lastName, isActive
// Make all required (remove null/undefined)
type UserListItem = /* TODO */;
// 3. UserFormData - for edit forms
// Should have: firstName, lastName, emailAddress, phoneNumber
// All fields optional
type UserFormData = /* TODO */;
// 4. UserCache - map of users by id
// Record<number, User> but User fields are optional
type UserCache = /* TODO */;
// TODO: Implement transformation functions
function transformApiUser(apiUser: ApiUser): User {
// TODO: Transform snake_case to camelCase and parse dates
}
function getUserListItem(user: User): UserListItem {
// TODO: Extract and ensure no nulls
}
function createUserCache(users: User[]): UserCache {
// TODO: Create map by id
}
// Tests - uncomment after implementation
// const apiUser: ApiUser = {
// id: 1,
// first_name: "John",
// last_name: "Doe",
// email_address: "john@example.com",
// phone_number: "+1234567890",
// is_active: true,
// created_at: "2024-01-01T00:00:00Z",
// updated_at: "2024-01-02T00:00:00Z"
// };
// const user = transformApiUser(apiUser);
// const listItem = getUserListItem(user);
// const cache = createUserCache([user]);
Solution:
Click to see solution
interface ApiUser {
id: number;
first_name: string;
last_name: string;
email_address: string;
phone_number: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
interface User {
id: number;
firstName: string;
lastName: string;
emailAddress: string;
phoneNumber: string | null;
isActive: boolean;
createdAt: Date;
updatedAt: Date;
}
// 2. List item - pick and make required
type UserListItem = Required<Pick<User, "id" | "firstName" | "lastName" | "isActive">>;
// 3. Form data - pick and make optional
type UserFormData = Partial<Pick<User, "firstName" | "lastName" | "emailAddress" | "phoneNumber">>;
// 4. Cache - record with partial values
type UserCache = Record<number, Partial<User>>;
// Implementations
function transformApiUser(apiUser: ApiUser): User {
return {
id: apiUser.id,
firstName: apiUser.first_name,
lastName: apiUser.last_name,
emailAddress: apiUser.email_address,
phoneNumber: apiUser.phone_number,
isActive: apiUser.is_active,
createdAt: new Date(apiUser.created_at),
updatedAt: new Date(apiUser.updated_at)
};
}
function getUserListItem(user: User): UserListItem {
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
isActive: user.isActive
};
}
function createUserCache(users: User[]): UserCache {
const cache: UserCache = {};
users.forEach(user => {
cache[user.id] = user;
});
return cache;
}
// Tests
const apiUser: ApiUser = {
id: 1,
first_name: "John",
last_name: "Doe",
email_address: "john@example.com",
phone_number: "+1234567890",
is_active: true,
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-02T00:00:00Z"
};
const user = transformApiUser(apiUser);
console.log(user.firstName); // "John"
const listItem = getUserListItem(user);
console.log(listItem); // { id: 1, firstName: "John", lastName: "Doe", isActive: true }
const cache = createUserCache([user]);
console.log(cache[1]?.firstName); // "John"
Exercise 3: State Management with Utility Types (8 min)
Create a type-safe state management system.
// Application state
interface AppState {
user: {
id: string;
name: string;
email: string;
} | null;
theme: "light" | "dark";
notifications: {
id: string;
message: string;
read: boolean;
}[];
settings: {
language: "en" | "pl" | "de";
timezone: string;
emailNotifications: boolean;
};
}
// TODO: Create utility types for state operations
// 1. StateUpdate - partial state for updates
type StateUpdate = /* TODO: Partial<AppState> */;
// 2. UserUpdate - partial user for user updates
type UserUpdate = /* TODO: Partial of user object */;
// 3. SettingsKeys - union of all settings keys
type SettingsKeys = /* TODO: keyof AppState["settings"] */;
// 4. ReadonlyState - readonly version of state
type ReadonlyState = /* TODO: Readonly<AppState> */;
// TODO: Implement state management functions
function updateState(current: AppState, updates: StateUpdate): AppState {
// TODO: Merge updates into current state
}
function updateUser(current: AppState, updates: UserUpdate): AppState {
// TODO: Update user in state
}
function getSetting<K extends SettingsKeys>(
state: AppState,
key: K
): AppState["settings"][K] {
// TODO: Get specific setting
}
// Tests - uncomment after implementation
// const initialState: AppState = {
// user: { id: "1", name: "John", email: "john@example.com" },
// theme: "light",
// notifications: [],
// settings: {
// language: "en",
// timezone: "UTC",
// emailNotifications: true
// }
// };
// const updated = updateState(initialState, { theme: "dark" });
// const withUser = updateUser(initialState, { name: "Jane" });
// const language = getSetting(initialState, "language");
Solution:
Click to see solution
interface AppState {
user: {
id: string;
name: string;
email: string;
} | null;
theme: "light" | "dark";
notifications: {
id: string;
message: string;
read: boolean;
}[];
settings: {
language: "en" | "pl" | "de";
timezone: string;
emailNotifications: boolean;
};
}
// 1. Partial state for updates
type StateUpdate = Partial<AppState>;
// 2. Partial user
type UserUpdate = Partial<NonNullable<AppState["user"]>>;
// 3. Settings keys
type SettingsKeys = keyof AppState["settings"];
// 4. Readonly state
type ReadonlyState = Readonly<AppState>;
// Implementations
function updateState(current: AppState, updates: StateUpdate): AppState {
return {
...current,
...updates
};
}
function updateUser(current: AppState, updates: UserUpdate): AppState {
if (!current.user) {
throw new Error("No user to update");
}
return {
...current,
user: {
...current.user,
...updates
}
};
}
function getSetting<K extends SettingsKeys>(
state: AppState,
key: K
): AppState["settings"][K] {
return state.settings[key];
}
// Tests
const initialState: AppState = {
user: { id: "1", name: "John", email: "john@example.com" },
theme: "light",
notifications: [],
settings: {
language: "en",
timezone: "UTC",
emailNotifications: true
}
};
const updated = updateState(initialState, { theme: "dark" });
console.log(updated.theme); // "dark"
const withUser = updateUser(initialState, { name: "Jane" });
console.log(withUser.user?.name); // "Jane"
const language = getSetting(initialState, "language");
console.log(language); // "en"
// Type-safe - these would error:
// getSetting(initialState, "invalid"); // ❌ Error
// updateState(initialState, { theme: "blue" }); // ❌ Error
Quick Review - Key Concepts
1. When to use Partial vs Required?
Answer
Partial
Required
When to use Partial:
- Update operations (PATCH endpoints)
- Optional configuration overrides
- Form data where not all fields are filled
- Incremental data building
Example:
interface User {
id: number;
name: string;
email: string;
}
// Update function - can update any fields
function updateUser(id: number, updates: Partial<User>): User {
const existing = getUser(id);
return { ...existing, ...updates };
}
updateUser(1, { name: "John" }); // ✅ Only name
When to use Required:
- Validation functions that need all fields
- Converting optional config to full config with defaults
- Ensuring complete data before processing
Example:
interface Config {
apiUrl?: string;
timeout?: number;
}
function validateConfig(config: Required<Config>): boolean {
// All fields guaranteed to exist
return config.apiUrl.length > 0 && config.timeout > 0;
}
Key difference: Partial makes things more flexible, Required makes things more strict.
2. Pick vs Omit - which one to use?
Answer
Pick<T, K> - select specific properties (whitelist approach)
Omit<T, K> - exclude specific properties (blacklist approach)
When to use Pick:
- You want only a few fields from a large type
- Creating focused DTOs/views
- When the selected fields are the important part
Example:
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// Public profile - pick what to show
type PublicProfile = Pick<User, "id" | "name">;
When to use Omit:
- You want most fields except a few
- Removing sensitive data
- When excluded fields are the exception
Example:
// Remove only password
type UserDTO = Omit<User, "password">;
// Remove auto-generated fields for create input
type UserCreateInput = Omit<User, "id" | "createdAt" | "updatedAt">;
Decision rule:
- If selecting < 50% of fields → use Pick
- If selecting > 50% of fields → use Omit
- For sensitive data removal → always Omit (more explicit)
3. What is Record and when to use it?
Answer
Record<K, T> creates an object type with keys of type K and values of type T.
Syntax:
type MyRecord = Record<KeyType, ValueType>;
When to use:
- Dictionaries/Maps with known key types:
type UserCache = Record<string, User>;
const cache: UserCache = {
"user1": { id: 1, name: "John" },
"user2": { id: 2, name: "Jane" }
};
- Enum-like mappings:
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
const permissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
- Configuration objects:
type Environment = "dev" | "staging" | "prod";
type Config = Record<Environment, { apiUrl: string; debug: boolean }>;
const config: Config = {
dev: { apiUrl: "http://localhost:3000", debug: true },
staging: { apiUrl: "https://staging.api.com", debug: true },
prod: { apiUrl: "https://api.com", debug: false }
};
- Translation files:
type TranslationKey = "welcome" | "goodbye" | "error";
type Translations = Record<TranslationKey, string>;
const en: Translations = {
welcome: "Welcome",
goodbye: "Goodbye",
error: "Error occurred"
};
Benefits:
- Type-safe keys
- Ensures all keys are present
- Better than index signature for known keys
Record vs Index Signature:
// Index signature - any string key
type Dict1 = { [key: string]: number };
// Record - more explicit, better for specific keys
type Dict2 = Record<"a" | "b" | "c", number>;
4. Exclude vs Extract - what’s the difference?
Answer
Both work on union types but in opposite ways:
Exclude<T, U> - removes types from union (blacklist)
Extract<T, U> - keeps only specified types (whitelist)
Exclude example:
type Status = "idle" | "loading" | "success" | "error";
// Remove error states
type ValidStatus = Exclude<Status, "error">;
// "idle" | "loading" | "success"
// Remove multiple
type ActiveStatus = Exclude<Status, "idle" | "error">;
// "loading" | "success"
Extract example:
type Status = "idle" | "loading" | "success" | "error";
// Keep only loading states
type LoadingStatus = Extract<Status, "idle" | "loading">;
// "idle" | "loading"
// Keep only one
type ErrorOnly = Extract<Status, "error">;
// "error"
Practical use cases:
Exclude:
// Remove dangerous HTTP methods
type AllMethods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type SafeMethods = Exclude<AllMethods, "DELETE">;
// Remove null/undefined
type NonNullable<T> = Exclude<T, null | undefined>;
Extract:
// Get only function types
type Mixed = string | number | (() => void) | boolean;
type Functions = Extract<Mixed, Function>;
// () => void
// Filter discriminated union
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
type Circles = Extract<Shape, { kind: "circle" }>;
// { kind: "circle"; radius: number }
Memory aid:
- Exclude = Except these (remove)
- Extract = Extract only these (keep)
5. How to combine multiple utility types effectively?
Answer
Utility types can be composed to create complex transformations. Key is to think in layers.
Common patterns:
1. Update DTO (editable fields only):
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Can update some fields, but not id/timestamp
type UserUpdateDTO = Partial<Omit<User, "id" | "createdAt">>;
// {
// name?: string;
// email?: string;
// password?: string;
// }
2. Public API response (safe fields only):
// Remove sensitive, make readonly
type PublicUser = Readonly<Omit<User, "password">>;
// {
// readonly id: string;
// readonly name: string;
// readonly email: string;
// readonly createdAt: Date;
// }
3. Required subset:
// Pick fields and make them required
type UserCredentials = Required<Pick<User, "email" | "password">>;
// {
// email: string;
// password: string;
// }
4. Partial cache:
// Map of entities with optional fields
type UserCache = Record<string, Partial<User>>;
const cache: UserCache = {
"user1": { id: "1", name: "John" }, // Some fields
"user2": { email: "jane@email.com" } // Different fields
};
5. Deep transformations:
// Make nested objects readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
// Make all fields nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Combine multiple transformations
type SafeUpdateDTO = Partial<Nullable<Omit<User, "id">>>;
Best practices:
- Start with the base type
- Apply transformations from inside out
- Use descriptive type names
- Document complex combinations
- Consider extracting reusable patterns
Reading order: Right to left
Partial<Omit<User, "id">>
// 1. Omit "id" from User
// 2. Make result Partial
Checklist - What You Should Know After Day 5
- Use Partial and Required for flexible/strict types
- Apply Readonly to prevent mutations
- Select properties with Pick
- Exclude properties with Omit
- Create type-safe maps with Record
- Filter union types with Exclude and Extract
- Remove null/undefined with NonNullable
- Extract types from functions with ReturnType and Parameters
- Unwrap Promise types with Awaited
- Combine multiple utility types effectively
Quick Reference Card
// Property modifiers
Partial<T> // All properties optional
Required<T> // All properties required
Readonly<T> // All properties readonly
// Property selection
Pick<T, K> // Select specific properties
Omit<T, K> // Exclude specific properties
// Object creation
Record<K, T> // Create object with keys K and values T
// Union filtering
Exclude<T, U> // Remove U from T
Extract<T, U> // Keep only U from T
NonNullable<T> // Remove null and undefined
// Function utilities
ReturnType<T> // Extract return type
Parameters<T> // Extract parameters as tuple
ConstructorParameters<T> // Extract constructor params
// Promise utilities
Awaited<T> // Unwrap Promise type
// Combining utilities
Partial<Omit<User, "id">> // Update DTO
Readonly<Pick<User, "id" | "name">> // Read-only subset
Record<string, Partial<User>> // Flexible cache
Tomorrow: Mapped Types & Conditional Types - create your own type transformations