TypeScript utility types are built into the language's standard library, they ship with every TypeScript version, and most Angular developers underuse them. I have done a lot of code reviews over the years and the same pattern appears constantly: a developer writes a new interface that is almost identical to an existing one, differing only in which fields are required or which fields are included. That is exactly what Partial, Pick, Omit, and Required are for. Using them reduces duplication, improves refactorability, and communicates intent. This is what they actually look like in Angular code.
Partial: Stop Writing Separate Update Interfaces
Partial makes every property of a type optional. The canonical Angular use case is PATCH payloads and form partial updates. Before I internalized this, I was writing separate UpdateUser interfaces that were just User with everything optional. That is exactly what Partial does automatically.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface UserProfile {
id: string;
firstName: string;
lastName: string;
email: string;
timezone: string;
plan: 'free' | 'pro' | 'team';
seats: number;
}
@Injectable({ providedIn: 'root' })
export class UserProfileService {
private http = inject(HttpClient);
// No need for a separate UpdateUserProfile interface
updateProfile(id: string, changes: Partial<UserProfile>): Observable<UserProfile> {
return this.http.patch<UserProfile>(`/api/profiles/${id}`, changes);
}
}
// TypeScript enforces that only valid fields are passed
this.userProfileService.updateProfile(id, {
timezone: 'America/Toronto',
plan: 'pro'
});Required: Enforce Completeness at a Boundary
Required inverts Partial. Every optional property becomes non-optional. I use it at boundaries where I know a partially-constructed object should be complete before it proceeds. Test factories are the most common case. One thing people get wrong: Required only strips the question mark. It does not remove null or undefined from a property's union type. If a field is typed string | null, Required<T> keeps the null.
interface DraftOrder {
customerId?: string;
totalAmount?: number;
shippingMethod?: string;
itemCount?: number;
}
function validateComplete(draft: DraftOrder): Required<DraftOrder> {
const fields = ['customerId', 'totalAmount', 'shippingMethod', 'itemCount'] as const;
for (const field of fields) {
// Required removes the optional marker, so undefined is the only gap to check
if (draft[field] === undefined) {
throw new Error(`Order is missing required field: ${field}`);
}
}
return draft as Required<DraftOrder>;
}
// In tests, Required<T> enforces complete mock objects
function createMockOrder(): Required<DraftOrder> {
return {
customerId: 'cust-123',
totalAmount: 24900,
shippingMethod: 'standard',
itemCount: 3
};
}Pick: Narrow Component Inputs to What They Actually Use
Pick creates a new type containing only the keys you specify. For component inputs, this is more informative than passing the full domain object. It documents what the component actually reads and prevents components from reaching into fields they should not care about.
import { Component, Input } from '@angular/core';
interface Order {
id: string;
customerName: string;
totalAmount: number;
discountAmount: number | null;
status: 'pending' | 'paid' | 'shipped' | 'cancelled';
placedAt: Date;
shippedAt: Date | null;
fulfilledBy: string | null;
warehouseNotes: string;
weightKg: number;
}
// The card component needs none of the fulfilment fields
type OrderCardData = Pick<
Order,
'id' | 'customerName' | 'totalAmount' | 'status' | 'placedAt'
>;
@Component({ selector: 'app-order-card', standalone: true, template: '' })
export class OrderCardComponent {
@Input({ required: true }) order!: OrderCardData;
}Omit: Create vs Update Patterns
Omit is the complement to Pick. Give me everything except these keys. The most common Angular pattern is creation payloads where server-generated fields do not exist yet.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Order {
id: string;
status: 'pending' | 'paid' | 'shipped';
placedAt: Date;
shippedAt: Date | null;
// These come from the user
customerId: string;
totalAmount: number;
shippingMethod: string;
itemCount: number;
}
// Server generates id, status, placedAt, shippedAt
type CreateOrderPayload = Omit<
Order,
'id' | 'status' | 'placedAt' | 'shippedAt'
>;
@Injectable({ providedIn: 'root' })
export class OrdersService {
private http = inject(HttpClient);
createOrder(payload: CreateOrderPayload): Observable<Order> {
return this.http.post<Order>('/api/orders', payload);
}
}Record: Typed Lookup Maps
Record creates an object type with defined key and value types. I use it constantly for config objects, status maps, and any dictionary-style structure. It is safer than { [key: string]: something } because the keys are typed.
type OrderStatus = 'pending' | 'paid' | 'packing' | 'shipped' | 'cancelled';
const statusLabels: Record<OrderStatus, string> = {
'pending': 'Pending Payment',
'paid': 'Paid',
'packing': 'Packing',
'shipped': 'Shipped',
'cancelled': 'Cancelled'
};
// TypeScript enforces every status is covered
// Adding a new status to the union forces you to add the label
const statusColors: Record<OrderStatus, string> = {
'pending': '#f59e0b',
'paid': '#3b82f6',
'packing': '#8b5cf6',
'shipped': '#10b981',
'cancelled': '#9ca3af'
};ReturnType and Parameters: Working with Code You Don't Own
These two are powerful when you are working with third-party functions, auto-generated API clients, or legacy adapters where you cannot control the type definitions directly. ReturnType extracts what a function returns. Parameters extracts what it accepts.
import { Injectable, inject } from '@angular/core';
import { Observable, from, catchError } from 'rxjs';
// Working with a legacy adapter you cannot modify
declare function fetchShippingRates(
params: { postalCode: string; weightKg: number; carrier: 'standard' | 'express' }
): Promise<RawRateResponse>;
// Extract the types without duplicating them
type RateParams = Parameters<typeof fetchShippingRates>[0];
type RateResult = Awaited<ReturnType<typeof fetchShippingRates>>;
@Injectable({ providedIn: 'root' })
export class ShippingRatesService {
private errorHandler = inject(ApiErrorHandler);
// Use the extracted types in your Angular service wrapper
getRates(params: RateParams): Observable<RateResult> {
return from(fetchShippingRates(params)).pipe(
catchError(err => this.errorHandler.handle(err))
);
}
}Awaited is the piece that makes this usable. ReturnType of an async function gives you a Promise, and Awaited unwraps it, so the wrapper types the resolved value instead of the Promise. The classic case is an API client auto-generated from a Swagger (OpenAPI) spec. The generated types are not always what you need for the Angular layer. ReturnType and Parameters let you build wrappers without duplicating the generated type definitions, which would drift on every API regeneration.
Readonly: Enforce Immutability at the Type Level
Readonly makes every property non-reassignable. With NgRx selectors and signals, this reinforces the pattern that view code should never mutate state directly.
import { Component, inject } from '@angular/core';
import { Store, createSelector } from '@ngrx/store';
export const selectCurrentOrder = createSelector(
selectOrdersState,
(state): Readonly<Order> | null =>
state.currentOrder ? { ...state.currentOrder } : null
);
@Component({ selector: 'app-order-detail', standalone: true, template: '' })
export class OrderDetailComponent {
private store = inject(Store);
// The Readonly type prevents accidental mutation in the view layer
readonly order = this.store.selectSignal(selectCurrentOrder);
// TypeScript error if you try to mutate:
// this.order().status = 'shipped'; // Cannot assign to 'status' because it is a read-only property
}The jump from writing TypeScript to thinking in TypeScript shows up in exactly these places: reaching for a built-in utility instead of writing a new interface from scratch.
Start with Partial, Pick, and Omit. They appear in almost every service and component boundary once you are looking for them. Add Record for any lookup or config structure you build. Graduate to ReturnType and Parameters when you are wrapping code you cannot modify. These are not advanced type gymnastics. They are the standard toolkit of anyone writing maintainable TypeScript.
References
- TypeScript utility types. https://www.typescriptlang.org/docs/handbook/utility-types.html
- Angular @Input decorator. https://angular.dev/api/core/Input