From Zero to AI

Lesson 5.4: Utility Types

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Use Partial, Required, Readonly, and Pick
  • Transform types with Omit, Record, and Extract
  • Work with function types using Parameters, ReturnType
  • Combine utility types for complex transformations
  • Know when to use each utility type

What Are Utility Types?

Utility types are built-in generic types that transform other types. They save you from writing repetitive type definitions and make your code more expressive.

interface User {
  id: number;
  name: string;
  email: string;
}

// Without utility types - verbose
interface PartialUser {
  id?: number;
  name?: string;
  email?: string;
}

// With utility types - concise
type PartialUser = Partial<User>;

Partial

Makes all properties optional:

interface User {
  id: number;
  name: string;
  email: string;
}

type PartialUser = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
// }

// Perfect for update functions
function updateUser(id: number, updates: Partial<User>): User {
  const user = getUserById(id);
  return { ...user, ...updates };
}

// Only update what you need
updateUser(1, { name: 'New Name' });
updateUser(2, { email: 'new@example.com' });

Common Use Case: Form State

interface FormData {
  username: string;
  password: string;
  email: string;
}

// Form state starts empty
const formState: Partial<FormData> = {};

// Fill in as user types
formState.username = 'alice';
formState.email = 'alice@example.com';

// Check if complete before submit
function isComplete(form: Partial<FormData>): form is FormData {
  return form.username !== undefined && form.password !== undefined && form.email !== undefined;
}

Required

Makes all properties required (opposite of Partial):

interface Config {
  host?: string;
  port?: number;
  secure?: boolean;
}

type RequiredConfig = Required<Config>;
// Equivalent to:
// {
//   host: string;
//   port: number;
//   secure: boolean;
// }

// Ensure all config values are set
function initServer(config: Required<Config>) {
  console.log(`Starting server on ${config.host}:${config.port}`);
  console.log(`Secure: ${config.secure}`);
}

// Must provide all values
initServer({
  host: 'localhost',
  port: 3000,
  secure: true,
});

Readonly

Makes all properties readonly:

interface User {
  id: number;
  name: string;
  email: string;
}

type ReadonlyUser = Readonly<User>;
// Equivalent to:
// {
//   readonly id: number;
//   readonly name: string;
//   readonly email: string;
// }

const user: ReadonlyUser = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
};

// Error: Cannot assign to 'name' because it is a read-only property
// user.name = "Bob";

// Perfect for immutable state
function freeze<T>(obj: T): Readonly<T> {
  return Object.freeze(obj) as Readonly<T>;
}

Deep Readonly

Note that Readonly is shallow. For nested objects:

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;
// All nested properties are also readonly

Pick<T, K>

Creates a type with only the specified properties:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Equivalent to:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// Expose only safe fields
function getPublicProfile(user: User): PublicUser {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
  };
}

Use Case: API Responses

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  costPrice: number; // Internal only
  supplier: string; // Internal only
}

// Public API response
type ProductResponse = Pick<Product, 'id' | 'name' | 'description' | 'price'>;

function toProductResponse(product: Product): ProductResponse {
  return {
    id: product.id,
    name: product.name,
    description: product.description,
    price: product.price,
  };
}

Omit<T, K>

Creates a type without the specified properties (opposite of Pick):

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type SafeUser = Omit<User, 'password'>;
// Equivalent to:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// Also works with multiple keys
type UserWithoutSensitive = Omit<User, 'password' | 'email'>;
// { id: number; name: string; }

Use Case: Creating New Records

interface DatabaseRecord {
  id: number;
  createdAt: Date;
  updatedAt: Date;
  name: string;
  value: number;
}

// For creating new records (id and timestamps are auto-generated)
type NewRecord = Omit<DatabaseRecord, 'id' | 'createdAt' | 'updatedAt'>;
// { name: string; value: number; }

function createRecord(data: NewRecord): DatabaseRecord {
  return {
    id: generateId(),
    createdAt: new Date(),
    updatedAt: new Date(),
    ...data,
  };
}

createRecord({ name: 'Test', value: 42 });

Record<K, T>

Creates an object type with keys of type K and values of type T:

// Simple string keys
type Roles = Record<string, boolean>;
const userRoles: Roles = {
  admin: true,
  editor: false,
  viewer: true,
};

// Union type keys
type Status = 'pending' | 'active' | 'completed';
type StatusColors = Record<Status, string>;

const colors: StatusColors = {
  pending: 'yellow',
  active: 'green',
  completed: 'blue',
};

Use Case: Lookup Tables

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

interface HandlerConfig {
  handler: () => void;
  requiresAuth: boolean;
}

type RouteHandlers = Record<HttpMethod, HandlerConfig>;

const handlers: RouteHandlers = {
  GET: { handler: () => {}, requiresAuth: false },
  POST: { handler: () => {}, requiresAuth: true },
  PUT: { handler: () => {}, requiresAuth: true },
  DELETE: { handler: () => {}, requiresAuth: true },
};

Use Case: Grouping Data

interface User {
  id: number;
  name: string;
  department: string;
}

type UsersByDepartment = Record<string, User[]>;

function groupByDepartment(users: User[]): UsersByDepartment {
  return users.reduce((groups, user) => {
    const dept = user.department;
    if (!groups[dept]) {
      groups[dept] = [];
    }
    groups[dept].push(user);
    return groups;
  }, {} as UsersByDepartment);
}

Extract<T, U>

Extracts types from T that are assignable to U:

type AllTypes = string | number | boolean | null | undefined;

type Primitives = Extract<AllTypes, string | number | boolean>;
// string | number | boolean

type Nullish = Extract<AllTypes, null | undefined>;
// null | undefined

Use Case: Filtering Union Types

type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keypress'; key: string }
  | { type: 'scroll'; position: number }
  | { type: 'resize'; width: number; height: number };

// Extract events with coordinates
type PointEvent = Extract<Event, { x: number }>;
// { type: "click"; x: number; y: number }

// Extract events with a specific type
type ClickEvent = Extract<Event, { type: 'click' }>;
// { type: "click"; x: number; y: number }

Exclude<T, U>

Excludes types from T that are assignable to U (opposite of Extract):

type AllTypes = string | number | boolean | null | undefined;

type NonNullable = Exclude<AllTypes, null | undefined>;
// string | number | boolean

Use Case: Removing Specific Types

type Event =
  | { type: 'click'; x: number; y: number }
  | { type: 'keypress'; key: string }
  | { type: 'scroll'; position: number };

// All events except click
type NonClickEvent = Exclude<Event, { type: 'click' }>;
// { type: "keypress"; key: string } | { type: "scroll"; position: number }

NonNullable

Removes null and undefined from a type:

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

function processValue(value: string | null | undefined) {
  if (value !== null && value !== undefined) {
    const safe: NonNullable<typeof value> = value;
    console.log(safe.toUpperCase());
  }
}

Parameters

Extracts the parameter types of a function as a tuple:

function createUser(name: string, age: number, isAdmin: boolean) {
  return { name, age, isAdmin };
}

type CreateUserParams = Parameters<typeof createUser>;
// [string, number, boolean]

// Use spread to call with array
const params: CreateUserParams = ['Alice', 30, false];
const user = createUser(...params);

Use Case: Wrapping Functions

function originalFunction(a: string, b: number): boolean {
  return a.length > b;
}

function wrapper(
  ...args: Parameters<typeof originalFunction>
): ReturnType<typeof originalFunction> {
  console.log('Calling with:', args);
  return originalFunction(...args);
}

wrapper('hello', 3); // Logs: "Calling with: ["hello", 3]", returns true

ReturnType

Extracts the return type of a function:

function getUser() {
  return {
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
  };
}

type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }

// Useful for inferring types from existing functions
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json() as Promise<{ items: string[] }>;
}

type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// { items: string[] }

Awaited

Extracts the type from a Promise:

type PromiseString = Promise<string>;
type ResolvedString = Awaited<PromiseString>;
// string

// Works with nested promises
type NestedPromise = Promise<Promise<number>>;
type ResolvedNested = Awaited<NestedPromise>;
// number

// Practical usage with async functions
async function fetchUser(): Promise<{ id: number; name: string }> {
  return { id: 1, name: 'Alice' };
}

type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }

Combining Utility Types

The real power comes from combining utility types:

Example 1: Partial Update with Pick

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  settings: {
    theme: string;
    notifications: boolean;
  };
}

// Only allow updating certain fields, and make them optional
type UserUpdate = Partial<Pick<User, 'name' | 'email' | 'settings'>>;

function updateUser(id: number, updates: UserUpdate) {
  // Can update name, email, or settings - all optional
}

updateUser(1, { name: 'New Name' });
updateUser(1, { settings: { theme: 'dark', notifications: true } });

Example 2: Required Fields from Partial

interface FormData {
  username?: string;
  password?: string;
  email?: string;
  bio?: string;
}

// Some fields are required for submission
type SubmissionData = Required<Pick<FormData, 'username' | 'password' | 'email'>> &
  Pick<FormData, 'bio'>;
// { username: string; password: string; email: string; bio?: string }

Example 3: Readonly API Responses

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type ImmutableResponse<T> = Readonly<ApiResponse<Readonly<T>>>;

interface User {
  id: number;
  name: string;
}

type UserResponse = ImmutableResponse<User>;
// All properties are readonly, including nested User properties

Example 4: Omit and Record Together

interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

// Create a record of todos by id, without the id field in the value
type TodoMap = Record<number, Omit<Todo, 'id'>>;

const todos: TodoMap = {
  1: { title: 'Learn TypeScript', completed: true, createdAt: new Date() },
  2: { title: 'Build a project', completed: false, createdAt: new Date() },
};

Quick Reference Table

Utility Type Purpose Example
Partial<T> Make all properties optional Partial<User>
Required<T> Make all properties required Required<Config>
Readonly<T> Make all properties readonly Readonly<State>
Pick<T, K> Select specific properties Pick<User, "id" | "name">
Omit<T, K> Remove specific properties Omit<User, "password">
Record<K, T> Create object type Record<string, number>
Extract<T, U> Extract matching types Extract<A | B, A>
Exclude<T, U> Remove matching types Exclude<A | B, A>
NonNullable<T> Remove null/undefined NonNullable<string | null>
Parameters<T> Get function parameters Parameters<typeof fn>
ReturnType<T> Get function return type ReturnType<typeof fn>
Awaited<T> Unwrap Promise type Awaited<Promise<string>>

Exercises

Exercise 1: API Types

Create types for a user API using utility types:

interface User {
  id: number;
  username: string;
  email: string;
  password: string;
  createdAt: Date;
  updatedAt: Date;
}

// Create these types using utility types:
// 1. CreateUserInput - for creating users (no id, dates, or password visible)
// 2. UpdateUserInput - for updating users (optional username, email)
// 3. PublicUser - for API responses (no password)
Solution
interface User {
  id: number;
  username: string;
  email: string;
  password: string;
  createdAt: Date;
  updatedAt: Date;
}

// For creating users
type CreateUserInput = Pick<User, 'username' | 'email' | 'password'>;

// For updating users
type UpdateUserInput = Partial<Pick<User, 'username' | 'email'>>;

// For API responses
type PublicUser = Omit<User, 'password'>;

// Test
const createData: CreateUserInput = {
  username: 'alice',
  email: 'alice@example.com',
  password: 'secret123',
};

const updateData: UpdateUserInput = {
  email: 'newemail@example.com',
};

function getPublicUser(user: User): PublicUser {
  const { password, ...publicData } = user;
  return publicData;
}

Exercise 2: Form Validation

Create a type system for form validation:

interface FormFields {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
}

// Create:
// 1. FormErrors - each field can have an error message or be undefined
// 2. FormTouched - track which fields have been touched (boolean for each)
// 3. ValidatedForm - a form where all fields are required and readonly
Solution
interface FormFields {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
}

// Each field can have an error message
type FormErrors = Partial<Record<keyof FormFields, string>>;

// Track touched state for each field
type FormTouched = Record<keyof FormFields, boolean>;

// Validated form is complete and immutable
type ValidatedForm = Readonly<Required<FormFields>>;

// Usage
const errors: FormErrors = {
  email: 'Invalid email format',
  password: 'Password too short',
};

const touched: FormTouched = {
  username: true,
  email: true,
  password: false,
  confirmPassword: false,
};

function validateForm(fields: Partial<FormFields>): ValidatedForm | null {
  if (fields.username && fields.email && fields.password && fields.confirmPassword) {
    return Object.freeze({
      username: fields.username,
      email: fields.email,
      password: fields.password,
      confirmPassword: fields.confirmPassword,
    });
  }
  return null;
}

Exercise 3: Event Handler Types

Extract and use function types:

const eventHandlers = {
  onClick: (event: { x: number; y: number }) => {
    console.log(`Clicked at ${event.x}, ${event.y}`);
  },
  onKeyPress: (event: { key: string; code: number }) => {
    console.log(`Pressed ${event.key}`);
  },
  onSubmit: (data: { formId: string; values: Record<string, string> }) => {
    return { success: true, id: '123' };
  },
};

// Create types for:
// 1. ClickEventParam - the parameter type of onClick
// 2. SubmitResult - the return type of onSubmit
// 3. AllHandlers - a type representing all handler functions
Solution
const eventHandlers = {
  onClick: (event: { x: number; y: number }) => {
    console.log(`Clicked at ${event.x}, ${event.y}`);
  },
  onKeyPress: (event: { key: string; code: number }) => {
    console.log(`Pressed ${event.key}`);
  },
  onSubmit: (data: { formId: string; values: Record<string, string> }) => {
    return { success: true, id: '123' };
  },
};

// Parameter type of onClick
type ClickEventParam = Parameters<typeof eventHandlers.onClick>[0];
// { x: number; y: number }

// Return type of onSubmit
type SubmitResult = ReturnType<typeof eventHandlers.onSubmit>;
// { success: boolean; id: string }

// All handler types
type AllHandlers = typeof eventHandlers;

// Individual handler type
type OnClickHandler = AllHandlers['onClick'];
// (event: { x: number; y: number }) => void

// Use the extracted types
function triggerClick(param: ClickEventParam) {
  eventHandlers.onClick(param);
}

triggerClick({ x: 100, y: 200 });

Key Takeaways

  1. Utility types transform types: They create new types from existing ones
  2. Partial/Required: Toggle optionality of all properties
  3. Readonly: Make properties immutable
  4. Pick/Omit: Select or exclude specific properties
  5. Record: Create object types with specific key and value types
  6. Extract/Exclude: Filter union types
  7. Parameters/ReturnType: Work with function types
  8. Combine them: Real power comes from chaining utility types

Resources

Resource Type Description
TypeScript Handbook: Utility Types Documentation Complete list of utility types
TypeScript Utility Types Playground Tool Try utility types interactively
Type Challenges Practice Advanced type exercises

Next Lesson

Now let us put everything together and build a complete generic storage system.

Continue to Lesson 5.5: Practice - Generic Storage