DEV Community

Ojas Deshpande
Ojas Deshpande

Posted on • Edited on • Originally published at ojas-deshpande.com

TypeScript Utility Types Every Angular Developer Should Know

Introduction

TypeScript ships with a set of built-in generic types called utility types. They transform existing types into new ones — making properties optional, readonly, or required; picking or omitting specific keys; extracting return types from functions. They let you express complex type relationships without duplicating type definitions.

Most Angular developers use a handful of them — Partial and Observable come to mind — but the full set is underused relative to how much it can simplify type-heavy Angular code. This article covers the utility types that appear most frequently in Angular applications, with concrete examples drawn from the patterns you encounter every day: form handling, HTTP services, component inputs, NgRx state, and API response modeling.


Partial

Partial<T> makes all properties of T optional. Every property becomes T[key] | undefined.

The most common Angular use case is update payloads. When patching a resource, you only send the fields that changed — not the entire object.

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'viewer';
  lastLogin: Date;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  updateUser(id: string, changes: Partial<User>): Observable<User> {
    return this.http.patch<User>(`/api/users/${id}`, changes);
  }
}

// Caller only provides what changed — TypeScript enforces this
this.userService.updateUser(userId, { name: 'Jane', role: 'admin' });
Enter fullscreen mode Exit fullscreen mode

Partial also appears in form state modeling. A form in progress may not have all fields populated yet:

interface ProductForm {
  name: string;
  price: number;
  description: "string;"
  category: string;
}

// Form state before the user has filled everything in
type DraftProduct = Partial<ProductForm>;

// TypeScript won't complain about missing fields
const draft: DraftProduct = { name: 'Widget' };
Enter fullscreen mode Exit fullscreen mode

Required

Required<T> is the inverse of Partial — it makes all optional properties required. This is useful when you've modeled something with optional fields during construction but need to assert that all fields are present at a later stage.

interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}

function validateConfig(config: Config): Required<Config> {
  if (!config.apiUrl) throw new Error('apiUrl is required');
  if (!config.timeout) throw new Error('timeout is required');
  if (!config.retries) throw new Error('retries is required');

  // After validation, we assert the config is fully populated
  return config as Required<Config>;
}

// From this point on, TypeScript knows all fields are defined
const validated = validateConfig(rawConfig);
console.log(validated.apiUrl); // string, not string | undefined
Enter fullscreen mode Exit fullscreen mode

Readonly

Readonly<T> makes all properties of T non-writable. Attempts to mutate properties on a Readonly<T> type fail at compile time.

In Angular applications with OnPush change detection, immutability is the contract that makes the strategy work correctly. Readonly<T> lets you enforce this at the type level rather than just by convention:

interface AppState {
  users: User[];
  selectedUserId: string | null;
  isLoading: boolean;
}

// State is never mutated — only replaced
type ImmutableState = Readonly<AppState>;

function reducer(state: ImmutableState, action: Action): ImmutableState {
  switch (action.type) {
    case 'SET_LOADING':
      // Return new object — cannot mutate state directly
      return { ...state, isLoading: action.payload };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

For deeply nested immutability, Readonly<T> only applies at the top level — nested objects remain mutable. For deep immutability, use ReadonlyArray<T> for arrays and recursive Readonly wrappers, or a library like immer.


Pick

Pick<T, K> creates a new type containing only the properties of T whose keys are in the union K.

The most valuable Angular use case is creating view models from full entity types. Your API returns a rich User object, but a particular component only needs three fields:

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'viewer';
  createdAt: Date;
  lastLogin: Date;
  preferences: UserPreferences;
  address: Address;
}

// The user card only needs these three fields
type UserCardViewModel = Pick<User, 'id' | 'name' | 'email'>;

@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user!: UserCardViewModel;
  // Component can't accidentally access fields it shouldn't need
}
Enter fullscreen mode Exit fullscreen mode

This pattern enforces component encapsulation at the type level. A component that displays a user card has no business accessing preferences or address. Pick makes that boundary explicit and compiler-enforced.


Omit

Omit<T, K> is the complement of Pick — it creates a type containing all properties of T except those in K.

The canonical Angular use case is create payloads. When creating a new resource, server-generated fields like id and createdAt don't exist yet:

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'viewer';
  createdAt: Date;
}

// For creation, id and createdAt are server-generated
type CreateUserPayload = Omit<User, 'id' | 'createdAt'>;

@Injectable({ providedIn: 'root' })
export class UserService {
  createUser(payload: CreateUserPayload): Observable<User> {
    return this.http.post<User>('/api/users', payload);
  }
}

// TypeScript error if you try to pass id in the payload
this.userService.createUser({
  name: 'Jane',
  email: 'jane@example.com',
  role: 'viewer'
  // id: '...' — TypeScript error: Object literal may only specify known properties
});
Enter fullscreen mode Exit fullscreen mode

Record

Record<K, V> creates a type representing an object whose keys are of type K and whose values are of type V. It's the typed alternative to { [key: string]: V }.

In Angular applications, Record is useful for lookup tables, state maps, and any dictionary-style structure:

type UserId = string;
type LoadingState = 'idle' | 'loading' | 'success' | 'error';

// Track loading state per entity ID
type EntityLoadingMap = Record<UserId, LoadingState>;

// In an NgRx reducer or component state
interface UserState {
  entities: Record<UserId, User>;
  loadingStates: Record<UserId, LoadingState>;
  selectedId: UserId | null;
}
Enter fullscreen mode Exit fullscreen mode

Record with a union key type is particularly powerful — it constrains the valid keys to only the members of the union:

type Route = 'home' | 'products' | 'orders' | 'settings';
type RoutePermissions = Record<Route, boolean>;

const permissions: RoutePermissions = {
  home: true,
  products: true,
  orders: false,
  settings: false,
  // TypeScript error if any Route key is missing
  // TypeScript error if any key outside Route is added
};
Enter fullscreen mode Exit fullscreen mode

ReturnType

ReturnType<T> extracts the return type of a function type T. This is most useful when you want to derive a type from a function's output without declaring the type separately.

In Angular, this pattern appears most clearly with factory functions and NgRx selectors:

// A factory function that builds a complex object
function createUserViewModel(user: User, permissions: Permissions) {
  return {
    id: user.id,
    displayName: `${user.name} (${user.role})`,
    canEdit: permissions.includes('user:edit'),
    canDelete: permissions.includes('user:delete'),
  };
}

// Derive the type from the function without duplicating it
type UserViewModel = ReturnType<typeof createUserViewModel>;

// Now use it as an @Input type
@Component({ ... })
export class UserDetailComponent {
  @Input() viewModel!: UserViewModel;
}
Enter fullscreen mode Exit fullscreen mode

With NgRx selectors:

export const selectUserViewModel = createSelector(
  selectCurrentUser,
  selectPermissions,
  (user, permissions) => ({
    id: user.id,
    displayName: user.name,
    canEdit: permissions.canEditUsers,
  })
);

// Derive the output type from the selector
type UserViewModelState = ReturnType<typeof selectUserViewModel>;
Enter fullscreen mode Exit fullscreen mode

Parameters

Parameters<T> extracts the parameter types of a function as a tuple. Less commonly used than ReturnType, but valuable when you need to pass or store function arguments as typed values.

function searchUsers(
  query: string,
  filters: UserFilters,
  pagination: PaginationOptions
): Observable<PaginatedResult<User>> {
  return this.http.get<PaginatedResult<User>>('/api/users', {
    params: buildQueryParams(query, filters, pagination)
  });
}

// Extract parameter types without re-declaring them
type SearchParams = Parameters<typeof searchUsers>;
// SearchParams is [string, UserFilters, PaginationOptions]

// Useful for caching search arguments
function cacheSearch(params: SearchParams): void {
  const [query, filters, pagination] = params;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

NonNullable

NonNullable<T> removes null and undefined from a type union. Useful after null checks when you need to assert to TypeScript that a value is definitely present:

interface RouteData {
  userId: string | null;
  productId: string | undefined;
}

function processRoute(data: RouteData) {
  if (!data.userId) throw new Error('userId required');

  // Without NonNullable, TypeScript still thinks userId could be null here
  // because it doesn't narrow across function calls
  const userId: NonNullable<typeof data.userId> = data.userId;
  // userId is now typed as string
}
Enter fullscreen mode Exit fullscreen mode

A more practical Angular pattern — using NonNullable with the async pipe and strict null checks:

@Component({
  template: `
    @if (user$ | async; as user) {
      <!-- Inside this block, user is NonNullable -->
      <app-user-card [user]="user" />
    }
  `
})
export class UserPageComponent {
  user$: Observable<User | null> = this.store.select(selectCurrentUser);
}
Enter fullscreen mode Exit fullscreen mode

Extract and Exclude

Extract<T, U> extracts from T the types that are assignable to U. Exclude<T, U> does the opposite — it removes from T the types assignable to U.

These are most useful with union types:

type Status = 'idle' | 'loading' | 'success' | 'error';

// Only the terminal states (not loading or idle)
type TerminalStatus = Extract<Status, 'success' | 'error'>;
// TerminalStatus = 'success' | 'error'

// Everything except loading
type NonLoadingStatus = Exclude<Status, 'loading'>;
// NonLoadingStatus = 'idle' | 'success' | 'error'
Enter fullscreen mode Exit fullscreen mode

In Angular applications, this pattern is valuable for narrowing action types in effects or reducers:

type UserAction =
  | { type: 'LOAD_USER'; id: string }
  | { type: 'LOAD_USER_SUCCESS'; user: User }
  | { type: 'LOAD_USER_FAILURE'; error: string }
  | { type: 'UPDATE_USER'; changes: Partial<User> }
  | { type: 'DELETE_USER'; id: string };

// Only the actions that affect loading state
type LoadingActions = Extract<
  UserAction,
  { type: 'LOAD_USER' | 'LOAD_USER_SUCCESS' | 'LOAD_USER_FAILURE' }
>;
Enter fullscreen mode Exit fullscreen mode

Combining Utility Types

The real power comes from composing utility types. A few patterns that appear frequently in production Angular codebases:

Create vs Update payloads from a single interface:

interface Product {
  id: string;
  name: string;
  price: number;
  categoryId: string;
  createdAt: Date;
  updatedAt: Date;
}

type CreateProductPayload = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateProductPayload = Partial<Omit<Product, 'id' | 'createdAt' | 'updatedAt'>>;
Enter fullscreen mode Exit fullscreen mode

Safe subset for a component input:

// Only expose the fields a display component needs, as readonly
type ProductCardInput = Readonly<Pick<Product, 'id' | 'name' | 'price'>>;
Enter fullscreen mode Exit fullscreen mode

Discriminated union response type:

type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; message: string }
  | { status: 'loading' };

type SuccessResponse<T> = Extract<ApiResponse<T>, { status: 'success' }>;
// SuccessResponse<T> = { status: 'success'; data: T }
Enter fullscreen mode Exit fullscreen mode

Conclusion

TypeScript's built-in utility types are the vocabulary of type-level programming in Angular. They let you express the relationships between your types — between a full entity and its create payload, between a rich API response and the view model a component needs, between a union of all possible states and the subset a particular handler cares about — without duplicating type definitions.

The individual types are each simple. The discipline of reaching for them consistently — instead of writing any, loosening types, or duplicating interfaces — is what makes a large TypeScript codebase maintainable. Each utility type usage is a constraint you're encoding once and getting for free everywhere the type is used.

Top comments (1)

Collapse
 
kollittle profile image
kol kol

Utility types are great until you need to compose them. The real power move is combining Partial, Pick, and Omit to create precise type transformations for API responses.

One pattern I use often: Omit<User, 'password'> & { password?: string } for update DTOs — lets you send partial updates without accidentally exposing the password field in reads.

The mental model: treat types as transformations, not just descriptions.