top of page

TypeScript Utility Types Every Angular Developer Should Know

  • Writer: Ojas Deshpande
    Ojas Deshpande
  • Mar 15
  • 7 min read

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<T>


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' });

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' };

Required<T>


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

Readonly<T>


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;
  }
}

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<T, K>


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
}

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<T, K>


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
});

Record<K, V>


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;
}

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
};

ReturnType<T>


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;
}

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>;

Parameters<T>


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;
  // ...
}

NonNullable<T>


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
}

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);
}

Extract<T, U> and Exclude<T, U>


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'

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' }
>;

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'>>;

Safe subset for a component input:

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

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 }

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.

Comments


bottom of page