TypeScript

Utility Types

25 min Lesson 16 of 40

Utility Types in TypeScript

TypeScript provides several built-in utility types that facilitate common type transformations. These utilities allow you to manipulate and compose types in powerful ways, making your code more flexible and maintainable. In this lesson, we'll explore the most commonly used utility types and their practical applications.

Partial<T>

The Partial<T> utility type constructs a type with all properties of T set to optional. This is particularly useful when you need to work with objects that may have only a subset of properties defined.

TypeScript Code:
<interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// All properties become optional
type PartialUser = Partial<User>;

const updateUser = (id: number, updates: Partial<User>) => {
  // Can update only specific properties
  console.log(`Updating user ${id}`, updates);
};

// Valid calls
updateUser(1, { name: 'John' });
updateUser(2, { email: 'jane@example.com', age: 30 });
updateUser(3, {}); // Also valid
>
Tip: Partial<T> is perfect for update functions where you don't need to provide all properties of an object.

Required<T>

The opposite of Partial, Required<T> makes all properties in T required. This is useful when you have an interface with optional properties but need a version where all properties are mandatory.

TypeScript Code:
<interface UserProfile {
  username: string;
  bio?: string;
  avatar?: string;
  website?: string;
}

type CompleteProfile = Required<UserProfile>;

// Error: Missing required properties
const profile1: CompleteProfile = {
  username: 'johndoe'
  // Error: bio, avatar, and website are required
};

// Correct
const profile2: CompleteProfile = {
  username: 'johndoe',
  bio: 'Developer',
  avatar: 'avatar.jpg',
  website: 'https://example.com'
};
>

Readonly<T>

The Readonly<T> utility type constructs a type with all properties of T set to readonly, meaning the properties cannot be reassigned after initial assignment.

TypeScript Code:
<interface Config {
  apiUrl: string;
  timeout: number;
  retries: number;
}

const config: Readonly<Config> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};

// Error: Cannot assign to 'apiUrl' because it is a read-only property
config.apiUrl = 'https://new-api.example.com';

// For nested readonly, use recursive approach
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};
>
Warning: Readonly<T> only makes the first level readonly. For deeply nested objects, you need a custom recursive type.

Record<K, T>

The Record<K, T> utility type constructs an object type whose property keys are K and whose property values are T. It's useful for creating mapped types with specific keys.

TypeScript Code:
<type UserRole = 'admin' | 'editor' | 'viewer';

interface Permission {
  read: boolean;
  write: boolean;
  delete: boolean;
}

// Create an object with roles as keys and permissions as values
const rolePermissions: Record<UserRole, Permission> = {
  admin: { read: true, write: true, delete: true },
  editor: { read: true, write: true, delete: false },
  viewer: { read: true, write: false, delete: false }
};

// Another example: creating a dictionary
type PageTitle = Record<string, string>;

const pageTitles: PageTitle = {
  home: 'Home Page',
  about: 'About Us',
  contact: 'Contact Us'
};
>

Pick<T, K>

The Pick<T, K> utility type constructs a type by picking the set of properties K from T. This allows you to create a subset type with only specific properties.

TypeScript Code:
<interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  stock: number;
  category: string;
  createdAt: Date;
}

// Pick only the properties needed for display
type ProductPreview = Pick<Product, 'id' | 'name' | 'price'>;

const preview: ProductPreview = {
  id: 1,
  name: 'Laptop',
  price: 999.99
  // Other properties are not allowed
};

// Useful for API responses
type ProductListItem = Pick<Product, 'id' | 'name' | 'price' | 'stock'>;
>

Omit<T, K>

The Omit<T, K> utility type constructs a type by picking all properties from T and then removing K. It's the opposite of Pick.

TypeScript Code:
<interface User {
  id: number;
  username: string;
  password: string;
  email: string;
  createdAt: Date;
}

// Omit sensitive data for API responses
type PublicUser = Omit<User, 'password'>;

const publicUser: PublicUser = {
  id: 1,
  username: 'johndoe',
  email: 'john@example.com',
  createdAt: new Date()
  // password is not included
};

// Omit multiple properties
type UserCreateDTO = Omit<User, 'id' | 'createdAt'>;

const newUser: UserCreateDTO = {
  username: 'janedoe',
  password: 'securepass123',
  email: 'jane@example.com'
};
>
Note: Omit<T, K> is implemented as Pick<T, Exclude<keyof T, K>>, combining multiple utility types.

Exclude<T, U>

The Exclude<T, U> utility type constructs a type by excluding from T all union members that are assignable to U.

TypeScript Code:
<type AllStatus = 'pending' | 'approved' | 'rejected' | 'cancelled';

// Exclude cancelled status
type ActiveStatus = Exclude<AllStatus, 'cancelled'>;
// Result: 'pending' | 'approved' | 'rejected'

// Exclude multiple values
type PositiveStatus = Exclude<AllStatus, 'rejected' | 'cancelled'>;
// Result: 'pending' | 'approved'

// Works with types too
type Primitive = string | number | boolean | null | undefined;
type NonNullishPrimitive = Exclude<Primitive, null | undefined>;
// Result: string | number | boolean
>

Extract<T, U>

The Extract<T, U> utility type constructs a type by extracting from T all union members that are assignable to U. It's the opposite of Exclude.

TypeScript Code:
<type MixedType = string | number | boolean | null;

// Extract only string and number
type StringOrNumber = Extract<MixedType, string | number>;
// Result: string | number

// Extract specific values
type Status = 'success' | 'error' | 'pending' | 'idle';
type CompletedStatus = Extract<Status, 'success' | 'error'>;
// Result: 'success' | 'error'

// Practical example
type EventType = 'click' | 'scroll' | 'keydown' | 'keyup';
type KeyboardEvent = Extract<EventType, `key${string}`>;
// Result: 'keydown' | 'keyup'
>

NonNullable<T>

The NonNullable<T> utility type constructs a type by excluding null and undefined from T.

TypeScript Code:
<type MaybeString = string | null | undefined;

// Remove null and undefined
type DefiniteString = NonNullable<MaybeString>;
// Result: string

// Practical example
interface User {
  id: number;
  name: string;
  email: string | null;
}

const getEmail = (user: User): NonNullable<User['email']> => {
  if (user.email === null) {
    throw new Error('Email is required');
  }
  return user.email; // TypeScript knows this is string
};

// With function returns
type FunctionReturn = string | number | null | undefined;
type ValidReturn = NonNullable<FunctionReturn>;
// Result: string | number
>

ReturnType<T>

The ReturnType<T> utility type constructs a type consisting of the return type of function T. This is particularly useful when working with functions whose return types you want to reference.

TypeScript Code:
<function createUser(name: string, email: string) {
  return {
    id: Math.random(),
    name,
    email,
    createdAt: new Date()
  };
}

// Extract the return type
type User = ReturnType<typeof createUser>;
// Result: { id: number; name: string; email: string; createdAt: Date; }

// With async functions
async function fetchData() {
  return {
    data: [1, 2, 3],
    status: 'success' as const
  };
}

type FetchResult = ReturnType<typeof fetchData>;
// Result: Promise<{ data: number[]; status: 'success'; }>

// Unwrap Promise with Awaited
type UnwrappedResult = Awaited<ReturnType<typeof fetchData>>;
// Result: { data: number[]; status: 'success'; }
>

Combining Utility Types

The real power of utility types comes from combining them to create sophisticated type transformations.

TypeScript Code:
<interface Article {
  id: number;
  title: string;
  content: string;
  author: string;
  publishedAt: Date;
  updatedAt: Date;
  tags: string[];
}

// Create a type for article updates (optional fields, no id)
type ArticleUpdate = Partial<Omit<Article, 'id' | 'publishedAt'>>;

// Create a read-only version of specific fields
type ArticleDisplay = Readonly<Pick<Article, 'title' | 'content' | 'author'>>;

// Create a type for article creation (no id or timestamps)
type ArticleCreate = Omit<Article, 'id' | 'publishedAt' | 'updatedAt'>;

// Create a required version of a partial type
type RequiredUpdate = Required<Partial<Article>>;
// Same as Article, but demonstrates the concept
>
Exercise: Create a utility type that:
  1. Takes an interface with id, name, email, password, and createdAt properties
  2. Omits the password field
  3. Makes all remaining fields readonly
  4. Uses this type for a function that returns a safe user object

Custom Utility Types

You can create your own utility types by combining TypeScript's mapped types and conditional types.

TypeScript Code:
<// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type ProductWithOptionalDescription = PartialBy<Product, 'description'>;

// Make specific properties required
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Nullable utility
type Nullable<T> = { [K in keyof T]: T[K] | null };

// Deep partial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
>
Best Practice: Create reusable utility types in a dedicated types/utilities.ts file and import them across your project.

Practical Example: API Service

Complete Example:
<// Define base entity
interface BaseEntity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  username: string;
  email: string;
  password: string;
  isActive: boolean;
}

// Types for different operations
type UserCreate = Omit<User, keyof BaseEntity>;
type UserUpdate = Partial<Omit<User, keyof BaseEntity | 'password'>>;
type UserPublic = Omit<User, 'password'>;
type UserResponse = Readonly<UserPublic>;

class UserService {
  create(data: UserCreate): Promise<UserResponse> {
    // Implementation
    return Promise.resolve({} as UserResponse);
  }

  update(id: number, data: UserUpdate): Promise<UserResponse> {
    // Implementation
    return Promise.resolve({} as UserResponse);
  }

  findById(id: number): Promise<UserResponse | null> {
    // Implementation
    return Promise.resolve(null);
  }
}

// Usage
const service = new UserService();

service.create({
  username: 'johndoe',
  email: 'john@example.com',
  password: 'secure123',
  isActive: true
});

service.update(1, {
  email: 'newemail@example.com'
  // password cannot be updated here
});
>
Summary: Utility types are essential tools in TypeScript that help you write more maintainable and type-safe code. Master these built-in utilities and learn to combine them for complex type transformations.