Skip to main content

TypeScript Utility Types: From Partial to Awaited

Master TypeScript utility types with practical examples. Learn Partial, Required, Pick, Omit, Record, Awaited, and more — with real-world use cases that make your code type-safe and maintainable.

23 Nov 2025 11 min read
views
TypeScript Utility Types illustration

TypeScript Utility Types: From Partial to Awaited

TL;DR: TypeScript utility types are pre-built type transformations that save you from writing complex type logic from scratch. They’re like power tools for your type system — use them to make your code more maintainable and type-safe.

Ever found yourself writing the same type transformations over and over? Or copying properties from one type to create another?

TypeScript utility types are here to save the day. They’re built-in helpers that transform existing types into new ones, making your codebase more maintainable and your types more expressive.

Let’s dive into the most useful ones with real-world examples you’ll actually use.


What Are Utility Types?

Utility types are type-level functions that take one or more types and return a new type. Think of them as:

  • Transformers → They modify existing types
  • Composers → They combine types in useful ways
  • Time-savers → They eliminate repetitive type definitions

Instead of manually creating types, you use these helpers to derive them automatically.

// Manual type definition (tedious)
type UserUpdate = {
  name?: string;
  email?: string;
  age?: number;
};
 
// Using utility types (clean)
type UserUpdate = Partial<User>;

The Essential Utility Types

1. Partial<T> — Make Everything Optional

What it does: Makes all properties of a type optional.

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

Real-world use case: Update operations where you only want to change specific fields.

async function updateUser(id: string, updates: Partial<User>) {
  // Only update provided fields
  const existing = await getUser(id);
  return { ...existing, ...updates };
}
 
// All valid
updateUser("123", { name: "John" });
updateUser("123", { email: "new@email.com" });
updateUser("123", { name: "John", age: 30 });
💡

Pro tip: Combine Partial with Pick to make only specific fields optional:

type UserNameUpdate = Partial<Pick<User, 'name' | 'email'>>;

2. Required<T> — Make Everything Required

What it does: Makes all optional properties required (opposite of Partial).

type Config = {
  apiKey?: string;
  timeout?: number;
  retries?: number;
};
 
type StrictConfig = Required<Config>;
// {
//   apiKey: string;
//   timeout: number;
//   retries: number;
// }

Real-world use case: Enforcing all configuration values are provided.

function initializeApp(config: Required<Config>) {
  // TypeScript guarantees all fields are present
  console.log(config.apiKey); // No undefined check needed
  console.log(config.timeout);
  console.log(config.retries);
}
 
// TypeScript error
initializeApp({ apiKey: "key" });
 
// Valid
initializeApp({
  apiKey: "key",
  timeout: 5000,
  retries: 3
});

3. Readonly<T> — Make Everything Immutable

What it does: Makes all properties read-only.

type User = {
  name: string;
  email: string;
};
 
type ImmutableUser = Readonly<User>;
// {
//   readonly name: string;
//   readonly email: string;
// }
 
const user: ImmutableUser = { name: "John", email: "john@example.com" };
user.name = "Jane"; // TypeScript error: Cannot assign to 'name' because it is a read-only property

Real-world use case: Preventing accidental mutations in React state or API responses.

type ApiResponse<T> = Readonly<{
  data: T;
  status: number;
  timestamp: number;
}>;
 
function handleResponse(response: ApiResponse<User>) {
  // Safe: can't accidentally mutate
  console.log(response.data);
  // response.data = {}; // Error
}
📝

Note: Readonly is shallow. For deep immutability, you’d need recursive types or libraries like ts-toolbelt.


4. Pick<T, K> — Select Specific Properties

What it does: Creates a type by picking specific properties from another type.

type User = {
  id: string;
  name: string;
  email: string;
  age: number;
  role: string;
};
 
type UserProfile = Pick<User, 'name' | 'email' | 'age'>;
// {
//   name: string;
//   email: string;
//   age: number;
// }

Real-world use case: Creating view models or DTOs (Data Transfer Objects).

// API returns full user object
async function getUser(id: string): Promise<User> {
  // ... fetch from API
}
 
// But we only want to display name and email
function displayUser(user: Pick<User, 'name' | 'email'>) {
  return `${user.name} (${user.email})`;
}
 
const fullUser = await getUser("123");
displayUser(fullUser); // Works! TypeScript knows it has name and email

5. Omit<T, K> — Exclude Specific Properties

What it does: Creates a type by removing specific properties (opposite of Pick).

type User = {
  id: string;
  name: string;
  email: string;
  password: string; // Sensitive!
  createdAt: Date;
};
 
type PublicUser = Omit<User, 'password' | 'createdAt'>;
// {
//   id: string;
//   name: string;
//   email: string;
// }

Real-world use case: Removing sensitive data before sending to client.

function sanitizeUser(user: User): Omit<User, 'password' | 'internalId'> {
  const { password, internalId, ...safe } = user;
  return safe; // Type-safe removal
}
 
const user = await getUserFromDB("123");
const publicUser = sanitizeUser(user);
// publicUser.password // TypeScript error: Property doesn't exist
⚠️

Security tip: Always use Omit when removing sensitive fields. TypeScript will catch any accidental leaks at compile time.


6. Record<K, T> — Create Object Types

What it does: Creates an object type with specific keys and values.

type Status = 'pending' | 'approved' | 'rejected';
 
type StatusConfig = Record<Status, { color: string; icon: string }>;
// {
//   pending: { color: string; icon: string };
//   approved: { color: string; icon: string };
//   rejected: { color: string; icon: string };
// }
 
const config: StatusConfig = {
  pending: { color: 'yellow', icon: '' },
  approved: { color: 'green', icon: '' },
  rejected: { color: 'red', icon: '' }
};

Real-world use case: Configuration objects, enums with metadata, or API route handlers.

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
 
type RouteHandlers = Record<HttpMethod, (req: Request) => Response>;
 
const api: RouteHandlers = {
  GET: (req) => new Response('Get handler'),
  POST: (req) => new Response('Post handler'),
  PUT: (req) => new Response('Put handler'),
  DELETE: (req) => new Response('Delete handler')
  // Missing any method? TypeScript error!
};

7. Exclude<T, U> — Remove Types from Union

What it does: Removes specific types from a union type.

type AllStatus = 'pending' | 'approved' | 'rejected' | 'draft';
type ActiveStatus = Exclude<AllStatus, 'draft'>;
// 'pending' | 'approved' | 'rejected'

Real-world use case: Filtering out specific states or types.

type EventType = 'click' | 'hover' | 'focus' | 'blur' | 'error';
type UserInteraction = Exclude<EventType, 'error'>;
// 'click' | 'hover' | 'focus' | 'blur'
 
function trackInteraction(event: UserInteraction) {
  // Only user interactions, not errors
  analytics.track(event);
}

8. Extract<T, U> — Keep Only Matching Types

What it does: Keeps only types from a union that match another type (opposite of Exclude).

type AllStatus = 'pending' | 'approved' | 'rejected' | 'draft';
type FinalStatus = Extract<AllStatus, 'approved' | 'rejected'>;
// 'approved' | 'rejected'

Real-world use case: Extracting specific types from unions.

type ApiResponse = 
  | { status: 'success'; data: User }
  | { status: 'error'; message: string }
  | { status: 'loading' };
 
type SuccessResponse = Extract<ApiResponse, { status: 'success' }>;
// { status: 'success'; data: User }
 
function handleSuccess(response: SuccessResponse) {
  // TypeScript knows data exists
  console.log(response.data);
}

9. NonNullable<T> — Remove Null and Undefined

What it does: Removes null and undefined from a type.

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

Real-world use case: Filtering out null/undefined from arrays or function returns.

function getUsers(): (User | null)[] {
  // Some users might be null
  return [user1, null, user2, null, user3];
}
 
function getValidUsers(): NonNullable<User>[] {
  return getUsers().filter((user): user is NonNullable<typeof user> => 
    user !== null
  );
  // Return type is User[], not (User | null)[]
}

10. Parameters<T> — Extract Function Parameters

What it does: Extracts the parameter types from a function type.

function createUser(name: string, email: string, age: number) {
  return { name, email, age };
}
 
type CreateUserParams = Parameters<typeof createUser>;
// [string, string, number]

Real-world use case: Creating type-safe wrappers or middleware.

async function createUser(name: string, email: string, age: number) {
  // ... create user logic
}
 
// Wrapper that logs before calling
function loggedCreateUser(...args: Parameters<typeof createUser>) {
  console.log('Creating user with:', args);
  return createUser(...args);
}
 
// Type-safe: args match createUser's parameters
loggedCreateUser("John", "john@example.com", 30);

Advanced example: Type-safe API route handlers.

type Handler = (req: Request, res: Response) => Promise<void>;
 
function withAuth(handler: Handler) {
  return async (req: Request, res: Response) => {
    // Check authentication
    if (!isAuthenticated(req)) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    return handler(req, res);
  };
}
 
// Usage
const protectedHandler = withAuth(async (req, res) => {
  // TypeScript knows req and res types
  const userId = req.user.id; // Type-safe
});

11. ReturnType<T> — Extract Return Type

What it does: Extracts the return type from a function type.

function getUser(id: string): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}
 
type UserPromise = ReturnType<typeof getUser>;
// Promise<User>

Real-world use case: Deriving types from existing functions.

async function fetchUser(id: string) {
  const response = await fetch(`/api/users/${id}`);
  return response.json() as User;
}
 
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// User (after unwrapping Promise)
 
async function processUser(id: string) {
  const user: FetchedUser = await fetchUser(id);
  // TypeScript knows it's User, not Promise<User>
  console.log(user.name);
}

12. Awaited<T> — Unwrap Promises

What it does: Unwraps nested promises to get the final resolved type.

type UserPromise = Promise<User>;
type UserData = Awaited<UserPromise>;
// User
 
// Awaited also handles nested promises
type NestedPromise = Promise<Promise<User>>;
type Unwrapped = Awaited<NestedPromise>;
// User (unwraps both Promise layers)

Real-world use case: Working with async functions and their return types.

async function fetchUser(id: string): Promise<User> {
  // ... fetch logic
}
 
async function fetchUserWithCache(id: string): Promise<User> {
  // Returns cached user or fetches new one
  const cached = cache.get(id);
  return cached ?? fetchUser(id);
}
 
type CachedUser = Awaited<ReturnType<typeof fetchUserWithCache>>;
// User (unwraps Promise)
 
async function displayUser(id: string) {
  const user: CachedUser = await fetchUserWithCache(id);
  // TypeScript knows it's User
  console.log(user.name);
}

Pro tip: Awaited was introduced in TypeScript 4.5. Before that, you’d use ReturnType with conditional types. It’s especially useful with ReturnType:

type AsyncResult = Awaited<ReturnType<typeof asyncFunction>>;

Combining Utility Types

The real power comes from combining utility types:

Example 1: Partial Update with Specific Fields

type User = {
  id: string;
  name: string;
  email: string;
  password: string;
  role: string;
};
 
// Only allow updating name and email, and make them optional
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;
// {
//   name?: string;
//   email?: string;
// }

Example 2: Read-only Public API

type User = {
  id: string;
  name: string;
  email: string;
  password: string;
  internalNotes: string;
};
 
// Public user: read-only, without sensitive fields
type PublicUser = Readonly<Omit<User, 'password' | 'internalNotes'>>;
// {
//   readonly id: string;
//   readonly name: string;
//   readonly email: string;
// }

Example 3: Type-Safe Form State

type UserForm = {
  name: string;
  email: string;
  age: number;
};
 
// Form errors: same keys, but values are strings (error messages)
type FormErrors = Partial<Record<keyof UserForm, string>>;
// {
//   name?: string;
//   email?: string;
//   age?: string;
// }
 
function validateForm(form: UserForm): FormErrors {
  const errors: FormErrors = {};
  
  if (!form.name) errors.name = "Name is required";
  if (!form.email.includes('@')) errors.email = "Invalid email";
  if (form.age < 0) errors.age = "Age must be positive";
  
  return errors;
}

Advanced Patterns

Pattern 1: Deep Partial (Recursive)

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
 
type User = {
  profile: {
    name: string;
    bio: string;
  };
  settings: {
    theme: string;
  };
};
 
type UserUpdate = DeepPartial<User>;
// {
//   profile?: {
//     name?: string;
//     bio?: string;
//   };
//   settings?: {
//     theme?: string;
//   };
// }

Pattern 2: Make Specific Fields Required

type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
 
type User = {
  name?: string;
  email?: string;
  age?: number;
};
 
type UserWithEmail = RequireFields<User, 'email'>;
// {
//   name?: string;
//   email: string; // Required
//   age?: number;
// }

Pattern 3: Extract Async Function Return Type

type AsyncReturnType<T extends (...args: any) => Promise<any>> = 
  Awaited<ReturnType<T>>;
 
async function fetchData(): Promise<User> {
  // ...
}
 
type Data = AsyncReturnType<typeof fetchData>;
// User

Quick Reference Table

Utility TypeWhat It DoesCommon Use Case
Partial<T>Makes all properties optionalUpdate operations
Required<T>Makes all properties requiredStrict configuration
Readonly<T>Makes all properties readonlyImmutable data
Pick<T, K>Selects specific propertiesView models, DTOs
Omit<T, K>Removes specific propertiesRemoving sensitive data
Record<K, T>Creates object with specific keysConfiguration objects
Exclude<T, U>Removes types from unionFiltering union types
Extract<T, U>Keeps matching types from unionExtracting specific types
NonNullable<T>Removes null/undefinedFiltering arrays
Parameters<T>Extracts function parametersType-safe wrappers
ReturnType<T>Extracts return typeDeriving types from functions
Awaited<T>Unwraps promisesWorking with async functions

Best Practices

  1. Use utility types instead of manual types → They’re maintained by TypeScript and handle edge cases.

  2. Combine utility types → Chain them for complex transformations.

  3. Document complex type transformations → Add comments explaining why you’re using specific utilities.

  4. Prefer composition over duplication → Derive types from existing ones rather than copying.

  5. Use Awaited with async functions → Always unwrap promises when extracting return types.


Summary

TypeScript utility types are powerful tools that help you:

  • Reduce duplication → Derive types instead of copying
  • Improve type safety → Catch errors at compile time
  • Enhance maintainability → Types update automatically when base types change
  • Express intent → Code becomes more self-documenting

Start using them in your next project, and you’ll wonder how you ever lived without them! 🚀


💡

Next steps: Try implementing these utility types in your current project. Look for places where you’re manually creating types that could be derived using utility types instead.