TypeScript Utility Types: Partial, Pick, Omit, Record, ReturnType and Where Each Fits
Key takeaways
TypeScript utility types: Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, ReturnType, Parameters—API DTOs, forms, and state patterns.
Introduction
Utility types are type transformation tools that TypeScript provides out of the box. Rather than hand-writing interface CreateUserRequest { name: string; email: string; } every time you need a slightly different shape of an existing type, utility types let you derive that shape mechanically from User — so when User gains a field, every derived type stays correct automatically instead of silently drifting out of sync with hand-maintained duplicates. Almost every utility type covered below is implemented as a mapped type ({ [K in keyof T]: ... }), which is why each section also shows the “How it works” implementation — once that pattern clicks, you can read (and write) most of TypeScript’s built-in type transformations without memorizing them individually.
Partial
Concept
Makes every property optional.
interface User {
id: string;
name: string;
email: string;
age: number;
}
type PartialUser = Partial<User>;
// {
// id?: string;
// name?: string;
// email?: string;
// age?: number;
// }
Real-world example
function updateUser(id: string, updates: Partial<User>): User {
const user = getUser(id);
return { ...user, ...updates };
}
updateUser("U001", { name: "Alice" });
updateUser("U002", { email: "[email protected]", age: 30 });
This is the pattern Partial<T> earns its keep for: a PATCH-style update function where the caller legitimately only wants to change a subset of fields, but the function still needs the full User shape to type-check the merge ({ ...user, ...updates }) and the return value. Without Partial, you’d either need a separate hand-written UserUpdate interface (which duplicates User’s fields and drifts out of sync as User changes) or you’d lose type safety on updates entirely by typing it as any.
How it works
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
Required
Concept
Makes every property required.
interface User {
id: string;
name: string;
email?: string;
age?: number;
}
type RequiredUser = Required<User>;
// {
// id: string;
// name: string;
// email: string;
// age: number;
// }
How it works
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
Required<T> is the mirror image of Partial<T>, and the -? in its implementation is worth noticing specifically — ? in a mapped type marks a property optional, so -? is TypeScript’s syntax for subtracting that optional modifier, forcing every property to be present. This matters most at a validation boundary: after parsing untrusted input where every field is technically optional (Partial<Config> from a config file, say), running it through a runtime check and asserting the result as Required<Config> documents, in the type system, the exact point where “might be missing” becomes “guaranteed present.”
Readonly
Concept
Makes every property read-only.
interface User {
id: string;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = {
id: "U001",
name: "Alice",
email: "[email protected]"
};
// user.name = "Alice"; // ❌ Error
Worth being precise about what Readonly<T> actually protects: it’s a compile-time check only, enforced by the type checker, not a runtime immutability guarantee. Object.freeze(user) would give you the same error at runtime too (attempts to mutate silently fail in non-strict mode, or throw in strict mode), but Readonly<T> alone compiles away completely — there’s nothing stopping code that bypasses the type system ((user as any).name = "x") from mutating the object at runtime. Reach for Readonly<T> to catch accidental mutation during development and code review; reach for Object.freeze alongside it when you need the guarantee to hold even against code that isn’t type-checked.
How it works
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
Pick<T, K>
Concept
Selects only certain properties.
interface User {
id: string;
name: string;
email: string;
age: number;
address: string;
}
type UserPreview = Pick<User, "id" | "name">;
// {
// id: string;
// name: string;
// }
const preview: UserPreview = {
id: "U001",
name: "Alice"
};
Real-world example
type LoginForm = Pick<User, "email">;
type SignupForm = Pick<User, "name" | "email" | "age">;
Pick and Omit (next section) solve the same problem from opposite directions, and the choice between them is really about which list is shorter and more stable. Pick<User, "name" | "email" | "age"> is explicit about exactly what a signup form needs — safer when User might grow new sensitive fields later, since new fields are excluded by default rather than accidentally included. Omit<User, "password"> is more convenient when you want “everything except this one thing” and are fine with new fields flowing through automatically — which is usually right for a “public-safe” response type, and usually wrong for a form that should only ever collect a fixed, reviewed set of fields.
How it works
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
Omit<T, K>
Concept
Removes specific properties.
interface User {
id: string;
name: string;
email: string;
password: string;
}
type UserWithoutPassword = Omit<User, "password">;
// {
// id: string;
// name: string;
// email: string;
// }
const user: UserWithoutPassword = {
id: "U001",
name: "Alice",
email: "[email protected]"
};
Real-world example
type UserResponse = Omit<User, "password">;
type CreateUserRequest = Omit<User, "id">;
UserResponse = Omit<User, "password"> is one of the highest-value lines in this whole guide, security-wise — it makes “never send the password hash to the client” a type-level invariant instead of a habit someone has to remember at every API endpoint. If a handler accidentally returns the raw User object instead of the UserResponse-shaped one, TypeScript flags the extra password field as a type error at the call site, catching the leak before it ships rather than after a security review finds it in production logs.
How it works
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Record<K, T>
Concept
Builds an object type with fixed keys and a given value type.
type Role = "admin" | "user" | "guest";
type Permissions = Record<Role, string[]>;
const permissions: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};
Real-world example
type ErrorCode = "NOT_FOUND" | "UNAUTHORIZED" | "SERVER_ERROR";
type ErrorMessages = Record<ErrorCode, string>;
const errors: ErrorMessages = {
NOT_FOUND: "Resource not found",
UNAUTHORIZED: "Authentication required",
SERVER_ERROR: "A server error occurred"
};
type Language = "ko" | "en" | "ja";
type Translations = Record<Language, Record<string, string>>;
const translations: Translations = {
ko: { greeting: "안녕하세요", goodbye: "안녕히 가세요" },
en: { greeting: "Hello", goodbye: "Goodbye" },
ja: { greeting: "こんにちは", goodbye: "さようなら" }
};
Record<Role, string[]> does something { admin: string[], user: string[], guest: string[] } alone doesn’t: it forces every key of the Role union to be present. Forget the guest entry entirely, or typo it as gest, and TypeScript rejects the object at compile time — a plain object type with no Record wrapper would happily accept a partially-filled or misspelled object since it’s only checking that present keys have the right value type, not that all expected keys exist.
How it works
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
Exclude<T, U>
Concept
Removes types from a union.
type AllRoles = "admin" | "user" | "guest" | "moderator";
type NonAdminRoles = Exclude<AllRoles, "admin">;
// "user" | "guest" | "moderator"
let role: NonAdminRoles = "user";
Exclude operates on the union as a whole, not per-property the way Omit does on an object — this is the distinction to keep straight when both “removing something” verbs are in play. Use Omit when you’re removing keys from an object type; use Exclude when you’re removing members from a union of primitive values (string literals, in this case).
Extract<T, U>
Concept
Extracts only the members of a union that are assignable to U.
type AllRoles = "admin" | "user" | "guest" | "moderator";
type AdminRoles = Extract<AllRoles, "admin" | "moderator">;
// "admin" | "moderator"
let role: AdminRoles = "admin";
NonNullable
Concept
Removes null and undefined from a type.
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
let value: DefiniteString = "hello";
NonNullable earns its place most often after a lookup or a narrowing check the compiler can’t automatically propagate through — for example, stripping null | undefined from the result of Array.prototype.find, so downstream code that’s already confirmed the value exists doesn’t have to keep re-checking for null at every step.
ReturnType
Concept
Extracts a function’s return type.
function getUser() {
return {
id: "U001",
name: "Alice",
email: "[email protected]"
};
}
type User = ReturnType<typeof getUser>;
ReturnType<typeof getUser> — note the typeof — is deriving a type from a value (the function getUser), which is a different operation from everything above it in this guide, where you were always transforming an existing type into another type. This pairing is genuinely useful when a function’s return shape is the source of truth (common with ORM query results or third-party SDK calls where you don’t control or want to hand-maintain a matching interface) — instead of writing an interface that has to be kept in sync with the function by hand, ReturnType derives it and stays correct automatically as the function’s implementation evolves.
Parameters
Concept
Extracts a function’s parameter types as a tuple.
function createUser(name: string, age: number, email: string) {
return { name, age, email };
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number, string]
const params: CreateUserParams = ["Alice", 25, "[email protected]"];
createUser(...params);
Parameters<T> is the natural companion to ReturnType<T> for the same class of problem: instead of re-declaring a function’s argument list as a separate tuple type by hand (and risking it drifting out of sync when the function’s signature changes), you derive it. This shows up most often when wrapping or forwarding a call — a logging decorator, a retry helper, a memoization cache — that needs to accept “whatever arguments the wrapped function takes” without hardcoding that shape.
Practical examples
Example 1: API typing
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
type CreateUserRequest = Omit<User, "id" | "createdAt" | "updatedAt">;
type UpdateUserRequest = Partial<Omit<User, "id" | "createdAt" | "updatedAt">>;
type UserResponse = Omit<User, "password">;
type UserListItem = Pick<User, "id" | "name" | "email">;
Composing utility types, as UpdateUserRequest does (Partial<Omit<...>>), is where they really start paying for themselves — this one line says “everything a user can update, and none of it is required,” derived from the single User interface, without a fourth hand-written type to keep in sync. Four distinct API-facing shapes (create, update, response, list-item) all trace back to one source of truth, which is the whole point: change User once, and TypeScript tells you everywhere a derived type is now inconsistent, instead of that inconsistency silently shipping to production.
Example 2: Form state
interface FormField<T> {
value: T;
error: string | null;
touched: boolean;
}
type FormState<T> = {
[K in keyof T]: FormField<T[K]>;
};
interface LoginData {
email: string;
password: string;
}
type LoginFormState = FormState<LoginData>;
const form: LoginFormState = {
email: { value: "", error: null, touched: false },
password: { value: "", error: null, touched: false }
};
FormState<T> is a mapped type built on top of a generic (FormField<T>) rather than a built-in utility, which is worth noticing as the natural next step once Partial/Pick/Omit feel familiar: you’re not limited to the utilities TypeScript ships — the same [K in keyof T] mechanism they’re built from is available for your own domain-specific transformations, like “wrap every field of this data shape in form-tracking metadata.”
Example 3: App state slices
interface AppState {
user: User | null;
posts: Post[];
loading: boolean;
error: string | null;
}
type LoadingState = Pick<AppState, "loading">;
type ErrorState = Pick<AppState, "error">;
type DataState = Omit<AppState, "loading" | "error">;
Which utility type for which job
| Type | Use case | Example |
|---|---|---|
| Partial | Optional update payloads | PATCH bodies |
| Required | Ensure completeness | Validated records |
| Pick | Subsets | List rows, previews |
| Omit | Strip secrets | Responses without password |
| Record | Fixed key sets | Roles, error catalogs |
Related Articles
- TypeScript Getting Started | Install, Config, Syntax
- Advanced TypeScript Types | Unions, Intersections, Literals
- TypeScript Interfaces
- TypeScript Generics
- TypeScript Decorators
- Advanced TypeScript | Conditional Types, Template Literals
- TypeScript Project: REST API