Understanding TypeScript Utility Types
Partial, Required, Readonly, Pick, Omit, Record, ReturnType with real examples and the footguns I've hit.
Your User, UpdateUser, and PublicUser interfaces start copying each other. Utility types stop that drift.
They're built-in transforms on existing types. Small syntax, big payoff if you hate maintaining five nearly identical interfaces.
Partial
Makes every property optional. Perfect for PATCH bodies.
interface User {
id: number;
name: string;
email: string;
}
function updateUser(id: number, data: Partial<User>) {
// data can include any subset of User props
}
updateUser(1, { name: "Pantelis" });
Required
Flips optional fields to required. Handy after you've filled defaults.
interface Config {
cache?: boolean;
retries?: number;
}
const fullConfig: Required<Config> = {
cache: true,
retries: 3,
};
Readonly
Compile-time lock on reassignment. Runtime objects can still mutate unless you freeze them.
interface Settings {
theme: string;
language: string;
}
const appSettings: Readonly<Settings> = {
theme: "dark",
language: "en",
};
appSettings.theme = "light"; // error
Pick
Subset of keys.
type PublicUser = Pick<User, "id" | "name" | "email">;
Omit
Drop keys you must not expose.
type SafeUser = Omit<User, "password">;
Record
Typed map from keys to values.
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, string[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
ReturnType
Grab what a function returns without duplicating the shape.
function createUser() {
return { id: 1, name: "Pantelis", email: "dev@ptheodosiou.com" };
}
type UserReturn = ReturnType<typeof createUser>;
Gotchas
Partial is shallow. Nested objects stay strict unless you write a DeepPartial.
Readonly won't stop someone mutating at runtime.
Stacking ten Pick/Omit chains gets unreadable. Sometimes a new base interface is clearer.
Record<string, any> is just an escape hatch. Be specific when you can.
ReturnType on overloaded functions can infer a union you didn't expect.
Quick reference
| Type | Does | Typical use |
|---|---|---|
Partial<T> |
optional fields | PATCH |
Required<T> |
all required | after defaults |
Readonly<T> |
no reassignment | config |
Pick<T, K> |
keep keys | public DTO |
Omit<T, K> |
drop keys | hide secrets |
Record<K, T> |
key map | permissions |
ReturnType<T> |
fn return type | stay in sync |
Utility types won't fix bad modeling, but they cut duplicate interfaces and keep types aligned with real data. Less typing, fewer lies in your types.