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

24 min read

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 - makes all properties optional

Required - makes all properties 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:

  1. Dictionaries/Maps with known key types:
type UserCache = Record<string, User>;

const cache: UserCache = {
  "user1": { id: 1, name: "John" },
  "user2": { id: 2, name: "Jane" }
};
  1. 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"]
};
  1. 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 }
};
  1. 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

Back to blog