TypeScript Utility Types Every Angular Developer Should Know
- 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>
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