Cet article est publié en anglais.
Master TypeScript's Built-In Utility Types for Cleaner Code
Learn how TypeScript's utility types like Partial, Pick, Omit, and Record eliminate duplicate interfaces and keep type definitions in sync automatically.
Stop copying and pasting interfaces: write cleaner, bulletproof code with TypeScript's built-in type transformers.
Sooner or later, every TypeScript developer runs into the same wall. You design a clean User interface with eight fields. Minutes later, you need an edit form where only two fields are actually required, so you spin up an UpdateUserInput interface. Then comes an API response that must hide sensitive fields, so you add a PublicUser interface too. Before you know it, a good chunk of your codebase is made up of near-duplicate interfaces that quietly fall out of sync as the project grows.
Every time the underlying schema changes, you're forced to hunt down and update several places at once. Forget one, and you've just planted a bug that will surface later in production.
TypeScript solves this with a built-in toolkit called Utility Types. You can think of them as functions that operate on types instead of values: you feed in an existing type, apply some transformation rule, and get back a brand-new type. This lets you keep one authoritative definition and delegate all the repetitive reshaping work to the compiler.
Below are the utility types worth using constantly, broken down in plain language with realistic snippets you can adapt right away.
1. Transforming Property Strictness
These three utilities control whether fields are optional, required, or frozen.
Partial<Type>
In plain English: "Make every property optional."
Whenever you're building something like a PATCH endpoint or updating part of an application's state, you don't want to demand the entire object from the caller. Partial<T> walks through every field of an interface and adds a ? to each one.
interface UserProfile {
id: string;
name: string;
email: string;
avatarUrl: string;
theme: "light" | "dark";
}
// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;
function updateProfile(userId: string, changes: UpdateProfileInput) {
// Valid: changes can include any subset of UserProfile properties
apiClient.patch(`/users/${userId}`, changes);
}
// Valid call:
updateProfile("usr_101", { theme: "dark" });
Required<Type>
In plain English: "Remove every optional marker so nothing can be skipped."
Required<T> does the opposite of Partial<T>: given an interface where some fields are optional, it forces every one of them to be present. This comes in handy when you're merging in default settings and need to guarantee nothing is missing.
interface AppConfig {
apiUrl: string;
timeout?: number;
retries?: number;
enableLogging?: boolean;
}
// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;
const defaultConfig: ResolvedConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
enableLogging: true,
};
function initializeApp(userOptions: AppConfig): ResolvedConfig {
return { ...defaultConfig, ...userOptions };
}
Readonly<Type>
In plain English: "Freeze every property so it can't be reassigned."
When you want to guard against accidental mutation — think configuration values, component props, or shared application state — Readonly<T> flags every property as readonly. Any attempt to write to one of those keys afterward fails at compile time.
interface SystemRole {
roleName: string;
permissions: string[];
}
const SuperAdminRole: Readonly<SystemRole> = {
roleName: "SUPER_ADMIN",
permissions: ["read", "write", "delete", "admin"],
};
// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";
2. Carving Out What You Need
There are plenty of situations where you only need a slice of an interface. Rather than hand-writing a brand-new, unrelated type, you can slice pieces directly out of your existing domain models.
Pick<Type, Keys>
In plain English: "Build a new type using only these named keys from the original."
Say you have a large model, but a small UI widget or a lightweight database query only cares about a name and an avatar. You can instruct TypeScript to keep just those two fields.
interface Product {
id: string;
sku: string;
name: string;
price: number;
stockQuantity: number;
supplierId: string;
description: string;
}
// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;
const item: CartItemPreview = {
id: "prod_99",
name: "Wireless Mechanical Keyboard",
price: 129.99,
};
Omit<Type, Keys>
In plain English: "Take the whole original type, but drop these particular fields."
Omit<T, K> flips Pick around. When you're inserting a new record into a database, you typically have every field except the ones the system generates automatically, such as id or createdAt.
interface Article {
id: number;
title: string;
slug: string;
content: string;
publishedAt: Date;
viewCount: number;
}
// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;
function submitArticle(payload: CreateArticlePayload) {
// TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
apiClient.post("/articles", payload);
}
3. Dynamic Key Mapping
Record<Keys, Type>
In plain English: "Build a dictionary whose keys come from this list and whose values follow this shape."
Plain JavaScript objects used as lookup tables often end up typed loosely, something like { [key: string]: any }, which throws type safety out the window. Record<Keys, Type> instead locks down exactly which keys are allowed and what shape their values must take.
type SubscriptionTier = "free" | "pro" | "enterprise";
interface TierFeatures {
monthlyQuota: number;
customDomainAllowed: boolean;
supportLevel: "community" | "email" | "dedicated";
}
// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
free: {
monthlyQuota: 1000,
customDomainAllowed: false,
supportLevel: "community",
},
pro: {
monthlyQuota: 50000,
customDomainAllowed: true,
supportLevel: "email",
},
enterprise: {
monthlyQuota: 1000000,
customDomainAllowed: true,
supportLevel: "dedicated",
},
};
If a new "starter" tier gets added to SubscriptionTier down the line, TypeScript will flag subscriptionPlans right away and let you know the "starter" entry is missing.
4. Pruning and Narrowing Union Types
The utilities above reshape object keys. The next pair instead work on union types, such as "apple" | "banana" | "orange".
Exclude<UnionType, ExcludedMembers>
In plain English: "Take this set of possible values and remove the ones listed here."
type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";
// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"
Extract<UnionType, ExtractedMembers>
In plain English: "Take this set of possible values and keep only the ones matching this criteria."
Extract works in the reverse direction from Exclude: it isolates the overlap between two union types, keeping only the members that exist in both.
type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";
// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"
NonNullable<Type>
In plain English: strip null and undefined out of a type.
When you're dealing with data coming from external APIs or database clients, fields are frequently typed as string | null | undefined. NonNullable<T> clears away those empty-value possibilities so you're left with the meaningful types.
type SearchQuery = string | string[] | null | undefined;
type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]
5. Inferring Types from Existing Code
One of TypeScript's biggest time-savers is pulling types straight out of functions, promises, and imported modules instead of manually re-declaring them.
ReturnType<FunctionType>
In plain English: it surfaces whatever a function returns, so you never need to retype that shape by hand.
For complicated factory functions, Redux-style selectors, or third-party packages that don't expose their return shapes, you can grab the exact structure by referencing the function itself.
function buildSessionPayload(userId: string, roles: string[]) {
return {
sessionId: crypto.randomUUID(),
authenticatedAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 60 * 24,
account: {
id: userId,
primaryRole: roles[0] ?? "guest",
allRoles: roles,
},
};
}
// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;
If the object returned inside buildSessionPayload changes later, UserSession picks up the change automatically.
Parameters<FunctionType>
In plain English: it produces a tuple listing every argument a function accepts.
Whenever you're wrapping an SDK method, forwarding arguments, or building a logging layer around a function call, Parameters<T> ensures your wrapper's signature never drifts from the original.
function triggerAlert(message: string, severity: "low" | "high", code?: number) {
// Internal dispatch logic
}
// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;
function logAndAlert(...args: AlertArgs) {
console.log("Dispatching alert:", args[0]);
triggerAlert(...args);
}
Awaited<Type>
In plain English: it unwraps a Promise and exposes the value inside.
Since so much modern TypeScript revolves around async code, Awaited<T> is essential for peeling back one or more layers of nested promises until you reach the actual resolved value.
async function fetchAccountDetails() {
return {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
}
// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;
// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
6. Stacking Utility Types Like Building Blocks
Utility types show their real strength when combined. Since each one outputs a standard TypeScript type, you can feed the result of one directly into another.
Consider a data layer built around a Customer entity:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
createdAt: Date;
updatedAt: Date;
}
// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;
// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;
// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;
With just a few lines, you've produced three separate, fully type-checked data transfer objects. If the base Customer interface later gains a new property — say, loyaltyTier — all three derived types update on their own, with no manual edits required.
Quick Reference Summary
Treating types as hand-copied duplicates makes every schema change a maintenance burden. Utility types let you define your business models once and have the compiler derive every variation you need from that single definition. The result is leaner code, safer refactors, and less time spent reconciling near-identical interfaces so you can focus on building features.