TypeScript's utility types grew on me slowly. At first they felt like trivia — neat but useless. Then I started building real APIs and suddenly Omit and Pick were everywhere in my code.
So I wrote this as the reference that would've saved me weeks of trial and error: every built-in utility type, organized by what it actually does, with my honest take on how often each one comes up in practice. I'll also get into custom utility types toward the end, and finish with a real example that ties several of them together.
We'll use this User interface throughout most of the examples:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user" | "moderator";
createdAt: Date;
preferences: {
theme: "light" | "dark";
notifications: boolean;
};
}Partial, Required, and Readonly
Partial<T> makes every property on T optional. I use this one constantly — it's the natural fit for any "update" operation where you only want to change a few fields.
type UpdateUserDto = Partial<User>;
// Equivalent to:
// {
// id?: number;
// name?: string;
// email?: string;
// role?: "admin" | "user" | "moderator";
// createdAt?: Date;
// preferences?: { theme: "light" | "dark"; notifications: boolean };
// }
function updateUser(id: number, updates: Partial<User>): User {
const existingUser = getUserById(id);
return { ...existingUser, ...updates };
}
// Only update the name - perfectly valid
updateUser(1, { name: "New Name" });Required<T> does the opposite — it strips away all the ? modifiers and forces every property to be present. Honestly, I don't reach for this one much. It mostly comes up when you have a config type where everything is optional by default, but at some point you need to guarantee the full object exists.
interface Config {
host?: string;
port?: number;
database?: string;
ssl?: boolean;
}
type CompleteConfig = Required<Config>;
// All properties are now required
const config: CompleteConfig = {
host: "localhost",
port: 5432,
database: "myapp",
ssl: true
};Readonly<T> marks every property as readonly, which prevents reassignment after creation. I like this for config objects and anything returned from a store or cache — it signals "don't touch this."
type ImmutableUser = Readonly<User>;
const user: ImmutableUser = {
id: 1,
name: "Alice",
email: "[email protected]",
role: "admin",
createdAt: new Date(),
preferences: { theme: "dark", notifications: true }
};
// Error: Cannot assign to 'name' because it is a read-only property
user.name = "Bob";One gotcha: Readonly is shallow. That preferences object inside user? Still mutable. We'll look at a deep readonly version later.
Pick, Omit, and Record
These three are probably my most-used utility types. If you only learn a few, make it these.
Pick<T, K> creates a new type with just the properties you name. I use it all the time for shaping API responses — when a client only needs three fields from a ten-field object, Pick is the right call.
type UserPublicProfile = Pick<User, "id" | "name" | "role">;
// Equivalent to:
// { id: number; name: string; role: "admin" | "user" | "moderator" }
function getPublicProfile(user: User): UserPublicProfile {
return {
id: user.id,
name: user.name,
role: user.role
};
}Omit<T, K> is Pick's mirror — it grabs everything except the properties you list. This is my go-to for DTOs. When creating a new user, you probably don't want the caller providing id or createdAt:
type CreateUserDto = Omit<User, "id" | "createdAt">;
// Equivalent to:
// {
// name: string;
// email: string;
// role: "admin" | "user" | "moderator";
// preferences: { theme: "light" | "dark"; notifications: boolean };
// }
function createUser(data: CreateUserDto): User {
return {
id: generateId(),
createdAt: new Date(),
...data
};
}Record<K, T> builds an object type where the keys come from K and the values are all T. I reach for this whenever I need a lookup table or a dictionary. It's also great for making sure you've handled every case in a union:
type UserRole = "admin" | "user" | "moderator";
type RolePermissions = Record<UserRole, string[]>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete", "manage-users"],
user: ["read", "write"],
moderator: ["read", "write", "delete"]
};
// Record is also great for lookup tables
type StatusCodeMap = Record<number, string>;
const httpStatus: StatusCodeMap = {
200: "OK",
201: "Created",
404: "Not Found",
500: "Internal Server Error"
};Exclude, Extract, and NonNullable
These operate on union types rather than object types. They're built with conditional types under the hood, and they take a minute to wrap your head around.
Exclude<T, U> removes from union T any member assignable to U. I find this most useful when I have a broad union and need a narrower version of it.
type AllRoles = "admin" | "user" | "moderator" | "superadmin";
type BasicRoles = Exclude<AllRoles, "admin" | "superadmin">;
// Result: "user" | "moderator"
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;Extract<T, U> is the flip side — it keeps only the union members assignable to U. I've found it especially handy for pulling specific variants out of discriminated unions:
type StringOrNumber = Extract<string | number | boolean | object, string | number>;
// Result: string | number
// Practical example: extract event types
type AppEvent =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; offset: number }
| { type: "resize"; width: number; height: number };
type MouseEvent = Extract<AppEvent, { type: "click" }>;
// Result: { type: "click"; x: number; y: number }NonNullable<T> strips null and undefined from a type. Simple, but it comes up surprisingly often when you're chaining optional values or working with database results that might be null.
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// Result: string
function processValue(value: string | null | undefined): string {
const definite: NonNullable<typeof value> = value!;
return definite.toUpperCase();
}ReturnType, Parameters, ConstructorParameters, and Awaited
These utility types pull type information out of functions and promises. I'll be honest: ReturnType and Parameters are workhorses. The other two are more niche.
ReturnType<T> gives you whatever a function returns. This is the one I reach for when I don't want to manually duplicate a return type that some function already defines:
function fetchUsers() {
return {
users: [{ id: 1, name: "Alice" }],
total: 100,
page: 1
};
}
type FetchUsersResult = ReturnType<typeof fetchUsers>;
// Result: { users: { id: number; name: string }[]; total: number; page: number }
// Works with generic functions too
type JsonParseResult = ReturnType<typeof JSON.parse>;
// Result: anyParameters<T> extracts the parameter types as a tuple. This is really useful for wrapper functions where you want to accept the same arguments as the original:
function createUser(name: string, email: string, role: UserRole): User {
// implementation
return {} as User;
}
type CreateUserParams = Parameters<typeof createUser>;
// Result: [name: string, email: string, role: UserRole]
// Useful for creating wrapper functions
function logAndCreate(...args: Parameters<typeof createUser>): User {
console.log("Creating user with:", args);
return createUser(...args);
}ConstructorParameters<T> does the same thing but for class constructors. I've never needed this in practice outside of dependency injection setups, but when you need it, you need it:
class HttpClient {
constructor(
private baseUrl: string,
private timeout: number,
private headers: Record<string, string>
) {}
}
type HttpClientArgs = ConstructorParameters<typeof HttpClient>;
// Result: [baseUrl: string, timeout: number, headers: Record<string, string>]
function createClient(...args: ConstructorParameters<typeof HttpClient>) {
return new HttpClient(...args);
}Awaited<T> unwraps promises, recursively. Came in with TypeScript 4.5 and it's handy when you're combining ReturnType with async functions:
type A = Awaited<Promise<string>>;
// Result: string
type B = Awaited<Promise<Promise<number>>>;
// Result: number (recursively unwrapped)
async function fetchData(): Promise<{ items: string[]; count: number }> {
return { items: ["a", "b"], count: 2 };
}
type FetchDataResult = Awaited<ReturnType<typeof fetchData>>;
// Result: { items: string[]; count: number }Custom Utility Types: Mapped Types and Conditional Types
The built-in types are great, but the real fun starts when you write your own. Two features make this possible: mapped types and conditional types.
Mapped types let you iterate over keys and transform them. The classic use case is the deep version of Readonly that we talked about earlier:
// Deep Readonly - makes all nested properties readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
const user: DeepReadonly<User> = {
id: 1,
name: "Alice",
email: "[email protected]",
role: "admin",
createdAt: new Date(),
preferences: { theme: "dark", notifications: true }
};
// Error: Cannot assign to 'theme' because it is a read-only property
user.preferences.theme = "light";
// Deep Partial - makes all nested properties optional
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepPartial<T[K]>
: T[K];
};
// Nullable - makes all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;
// All properties can be nullConditional types follow a ternary pattern with the extends keyword. Combined with infer, they let you unpack types in pretty creative ways:
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Numbers = ElementOf<number[]>; // number
type Strings = ElementOf<string[]>; // string
// Flatten a type - if it's an array, get the element type
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Nested = Flatten<number[][][]>; // number
// Get the type of values in an object
type ValueOf<T> = T[keyof T];
type UserValues = ValueOf<User>;
// string | number | Date | "admin" | "user" | "moderator" | { theme: ... }
// Make specific properties required while keeping others the same
type RequireFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface SearchParams {
query?: string;
page?: number;
limit?: number;
sort?: string;
}
type RequiredSearch = RequireFields<SearchParams, "query" | "page">;
// query and page are required, limit and sort remain optionalTemplate Literal Types
These are probably the most fun part of TypeScript's type system. They let you construct string types using template literal syntax, and the results can be surprisingly expressive.
// Basic template literal types
type EventName = "click" | "scroll" | "keypress";
type EventHandler = `on${Capitalize<EventName>}`;
// Result: "onClick" | "onScroll" | "onKeypress"
// Generate getter and setter types
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface Point {
x: number;
y: number;
}
type PointGetters = Getters<Point>;
// { getX: () => number; getY: () => number }
type PointSetters = Setters<Point>;
// { setX: (value: number) => void; setY: (value: number) => void }
// CSS unit types
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSValue = `${number}${CSSUnit}`;
const width: CSSValue = "100px"; // Valid
const height: CSSValue = "50vh"; // Valid
// const bad: CSSValue = "wide"; // Error
// Parse route params from a string pattern
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/api/users/:userId/posts/:postId">;
// Result: "userId" | "postId"
type TypedRoute<T extends string> = {
params: Record<ExtractParams<T>, string>;
};
const route: TypedRoute<"/api/users/:userId/posts/:postId"> = {
params: {
userId: "123",
postId: "456"
}
};Template literal types show up in libraries like Express route typing and styled-components. They're one of those features that seem academic until you need one — and then nothing else will do.
Building a Type-Safe API Client
I want to end with a real example that combines multiple utility types into something you'd actually ship. This is a simplified version of an API client pattern I've used in production. It wires up Omit, Partial, conditional types, and generics so that every endpoint gets correct types for its request body and response — with zero runtime overhead.
// Define API endpoints
interface ApiEndpoints {
"/users": {
GET: { response: User[]; query: { page: number; limit: number } };
POST: { response: User; body: Omit<User, "id" | "createdAt"> };
};
"/users/:id": {
GET: { response: User; query: never };
PUT: { response: User; body: Partial<Omit<User, "id">> };
DELETE: { response: void; query: never };
};
}
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
// Extract endpoints that support a specific method
type EndpointsForMethod<M extends HttpMethod> = {
[K in keyof ApiEndpoints]: M extends keyof ApiEndpoints[K] ? K : never;
}[keyof ApiEndpoints];
// Type-safe fetch wrapper
type ApiResponse<Path extends keyof ApiEndpoints, Method extends keyof ApiEndpoints[Path]> =
ApiEndpoints[Path][Method] extends { response: infer R } ? R : never;
type ApiBody<Path extends keyof ApiEndpoints, Method extends keyof ApiEndpoints[Path]> =
ApiEndpoints[Path][Method] extends { body: infer B } ? B : never;
// Usage demonstrates complete type safety
async function apiClient<
P extends keyof ApiEndpoints,
M extends keyof ApiEndpoints[P] & HttpMethod
>(
path: P,
method: M,
body?: ApiBody<P, M>
): Promise<ApiResponse<P, M>> {
const response = await fetch(path as string, {
method: method as string,
body: body ? JSON.stringify(body) : undefined
});
return response.json();
}
// Fully typed - response is User[], body would be a type error
const users = await apiClient("/users", "GET");
// Fully typed - body must match Omit<User, "id" | "createdAt">
const newUser = await apiClient("/users", "POST", {
name: "Bob",
email: "[email protected]",
role: "user",
preferences: { theme: "light", notifications: false }
});
Comments (0)
No comments yet. Be the first to share your thoughts!