TypeScript Utility Types with Claude Code: Practical Beginner Guide
Learn Pick, Omit, Partial, Record, ReturnType, and Awaited with runnable Claude Code workflows.
TypeScript utility types are tools for turning an existing type into another type for a specific purpose.
If you copy a User type by hand into separate “public view”, “form input”, and “API update” types, those copies drift when a field changes.
Claude Code can help with the refactor, but you still need to understand the intent well enough to review what it generates.
This guide explains Pick, Omit, Partial, Required, Readonly, Record, ReturnType, and Awaited in beginner-friendly language.
Then it shows how to ask Claude Code for practical type design using copy-paste runnable examples.
Use the TypeScript Handbook Utility Types as the source of truth, and pair it with the TSConfig strict option when you want mistakes to surface early.
For related ClaudeCodeLab reading, start with practical TypeScript tips and the TypeScript generics guide.
The Plain-English Model
A utility type is a safe “copy and reshape” operation for types. Think of copying a spreadsheet tab, hiding columns, and marking some columns as optional before sharing it with another team. The difference is that TypeScript checks the reshaped type before the code runs.
Pick<User, "id" | "name"> selects only id and name from User.
Omit<User, "passwordHash"> keeps everything except passwordHash.
They look similar, but the reading direction is different.
Use Pick when the target type is intentionally small, and Omit when the target is mostly the original type with a few dangerous or irrelevant fields removed.
Partial<User> makes every property optional.
It is useful for drafts and PATCH inputs, but it can accidentally make email optional during account creation.
Required<User> goes the other way and makes optional properties required.
Readonly<User> prevents reassignment, which is useful for settings and master data.
Record<Keys, Type> creates a dictionary with known keys.
ReturnType<typeof fn> extracts a function’s return type.
Awaited<Promise<T>> extracts the value you get after await.
Together, Awaited<ReturnType<typeof fetchSomething>> can keep API functions and UI types in sync.
flowchart LR
A["Source type: User"] --> B["Pick: public view"]
A --> C["Omit: remove secrets"]
A --> D["Partial: update input"]
A --> E["Required: validated input"]
A --> F["Readonly: fixed settings"]
G["Function"] --> H["ReturnType"]
I["Promise"] --> J["Awaited"]
Quick Comparison
| Type | What it does | Practical use | Watch out for |
|---|---|---|---|
Pick<T, K> | Selects only specific keys | Lists, public profiles, cards | Keys not selected are not available |
Omit<T, K> | Removes specific keys | Create inputs, public output, logs | It does not remove runtime values |
Partial<T> | Makes all keys optional | Drafts, PATCH, in-progress forms | It is shallow |
Required<T> | Makes all keys required | Validated data before saving | It may require too much |
Readonly<T> | Prevents reassignment | Settings, permissions, constants | Nested objects need separate care |
Record<K, T> | Builds a known-key dictionary | Role permissions, labels, prices | Record<string, T> is often too broad |
ReturnType<T> | Gets a function return type | Sync API and UI types | Use typeof functionName |
Awaited<T> | Gets the resolved Promise value | Async function result types | It is not a runtime await |
Paste this table into Claude Code and ask it to review your type choices against the intent.
In a strict: true project, vague types fail faster, which is exactly what you want.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true
}
}
Use Case 1: Derive UI and Form Types from User
Admin screens often need several versions of the same entity. The database type, public display type, and form input type should not be hand-maintained copies. Keep one source type and derive the rest with utility types.
type UserRole = "admin" | "editor" | "viewer";
interface User {
id: string;
name: string;
email: string;
role: UserRole;
bio: string;
passwordHash: string;
createdAt: Date;
updatedAt: Date;
}
type PublicUser = Pick<User, "id" | "name" | "role" | "bio">;
type UserDraft = Partial<Omit<User, "id" | "passwordHash" | "createdAt" | "updatedAt">>;
type CreateUserInput =
Required<Pick<User, "name" | "email" | "role">> &
Partial<Pick<User, "bio">>;
function buildCreatePayload(input: CreateUserInput): Omit<User, "id" | "createdAt" | "updatedAt"> {
return {
name: input.name,
email: input.email,
role: input.role,
bio: input.bio ?? "",
passwordHash: "hashed-by-server",
};
}
const publicUser: PublicUser = {
id: "u_001",
name: "Masa",
role: "admin",
bio: "Claude Code workflow designer",
};
const draft: UserDraft = {
name: "Draft user",
bio: "Saved before email is confirmed",
};
console.log(publicUser);
console.log(buildCreatePayload({ name: "Aki", email: "aki@example.com", role: "editor" }));
console.log(draft);
Use a prompt that separates what remains, what is removed, and when a field is required.
Create public display, form draft, and create-API types from the User type.
Do not expose passwordHash.
For creation, require only name, email, and role. Keep bio optional.
Use Pick/Omit/Partial/Required and briefly explain why each utility type is used.
In production, pair these types with runtime validation such as Zod. Utility types check the shape before code runs; they do not prove that submitted form data is trustworthy.
Use Case 2: Lock Plan Features with Record
Pricing plans, roles, and status tables are ideal for Record.
It helps catch a missing team plan or a misspelled prioritySupport key during compilation.
type Plan = "free" | "pro" | "team";
type Feature = "exportPdf" | "inviteMember" | "prioritySupport";
const featureMatrix: Readonly<Record<Plan, Readonly<Record<Feature, boolean>>>> = {
free: {
exportPdf: false,
inviteMember: false,
prioritySupport: false,
},
pro: {
exportPdf: true,
inviteMember: false,
prioritySupport: false,
},
team: {
exportPdf: true,
inviteMember: true,
prioritySupport: true,
},
};
function canUse(plan: Plan, feature: Feature): boolean {
return featureMatrix[plan][feature];
}
console.log(canUse("pro", "exportPdf"));
console.log(canUse("free", "prioritySupport"));
Readonly documents that this table should not be mutated.
It is shallow by default, so this example marks the nested record as readonly too.
For deeper data, consider as const or a project-specific deep readonly helper.
Use Case 3: Reuse API Result Types with ReturnType and Awaited
When API client types and UI types are written separately, response changes become expensive.
ReturnType plus Awaited derives the result type from the async function itself.
async function fetchInvoice(invoiceId: string) {
return {
id: invoiceId,
status: "paid" as const,
amount: 48000,
currency: "JPY" as const,
paidAt: new Date("2026-06-02T10:00:00+09:00"),
};
}
type Invoice = Awaited<ReturnType<typeof fetchInvoice>>;
type InvoiceSummary = Pick<Invoice, "id" | "status" | "amount" | "currency">;
function formatInvoice(invoice: InvoiceSummary): string {
return `${invoice.id}: ${invoice.amount.toLocaleString()} ${invoice.currency} (${invoice.status})`;
}
async function main() {
const invoice = await fetchInvoice("inv_20260602");
console.log(formatInvoice(invoice));
}
main();
Tell Claude Code not to hand-write a duplicate response type when the API function is the trusted boundary. For external APIs and user input, still add runtime validation and error handling.
Use Case 4: Make PATCH Inputs Partial at the Right Level
Partial<T> is shallow.
It does not make nested object fields optional, which is a common beginner trap.
interface Profile {
id: string;
displayName: string;
settings: {
emailNotification: boolean;
smsNotification: boolean;
};
}
type ProfilePatch =
Omit<Partial<Profile>, "settings"> & {
settings?: Partial<Profile["settings"]>;
};
function patchProfile(current: Profile, patch: ProfilePatch): Profile {
return {
...current,
...patch,
settings: {
...current.settings,
...patch.settings,
},
};
}
const profile: Profile = {
id: "p_001",
displayName: "Masa",
settings: {
emailNotification: true,
smsNotification: false,
},
};
console.log(patchProfile(profile, { settings: { smsNotification: true } }));
If you used only ProfilePatch = Partial<Profile>, updating settings would still require the full settings object.
For beginner-friendly team code, a concrete type like this is often clearer than a clever generic deep partial.
Failure Cases to Avoid
Omit removes a key from the type, not from the runtime object.
If you return logs or API responses, you must actually strip the secret value.
interface Account {
id: string;
email: string;
passwordHash: string;
}
type SafeAccount = Omit<Account, "passwordHash">;
function toSafeAccount(account: Account): SafeAccount {
const { passwordHash, ...safeAccount } = account;
return safeAccount;
}
console.log(toSafeAccount({
id: "a_001",
email: "masa@example.com",
passwordHash: "secret",
}));
Record<string, T> is usually too wide for business rules.
Prefer a union such as type Plan = "free" | "pro" | "team" when the allowed keys are known.
Required<T> can make forms hostile if you apply it too early.
Use it after validation, when you really have complete data.
Awaited<T> describes a resolved Promise type.
It does not wait at runtime; you still need await or .then().
If you blur that line, Claude Code may tidy the type while forgetting loading and error states.
Claude Code Review Prompt
After implementation, ask for a risk-focused review instead of a vague cleanup.
Review this TypeScript type design.
1. Are Pick/Omit/Partial/Required/Readonly/Record matched to their use cases?
2. Are secrets removed at runtime, not only with Omit?
3. Does Partial make create inputs too loose?
4. Do ReturnType and Awaited reduce duplicated API types?
5. Are any vague any or broad string types left under strict settings?
This framing focuses Claude Code on preventing defects.
Masa hit this in a small admin screen: using Partial everywhere made an empty email look acceptable until the save flow.
Splitting “draft”, “create”, and “saved” into separate derived types made the generated patches much easier to review.
Conclusion
TypeScript utility types are not type-level gymnastics.
They are practical tools for expressing the difference between public data, drafts, validated inputs, fixed settings, and async API results.
Use Pick and Omit to control fields, Partial and Required to model workflow stages, Readonly and Record to protect configuration, and ReturnType plus Awaited to avoid duplicated response types.
ClaudeCodeLab helps teams apply Claude Code to TypeScript architecture, content CMS work, internal tools, and monetized product funnels. If your project has types but still ships avoidable mistakes, book Claude Code training and consulting.
I tested the examples in this article with a strict TypeScript mindset and found the biggest practical win in ReturnType plus Awaited: response changes become easier to locate.
The important caveat is Omit; it never removes secrets at runtime, so public response functions still need explicit object stripping.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.