Tips & Tricks (Updated: 6/2/2026)

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 with Claude Code: Practical Beginner Guide

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

TypeWhat it doesPractical useWatch out for
Pick<T, K>Selects only specific keysLists, public profiles, cardsKeys not selected are not available
Omit<T, K>Removes specific keysCreate inputs, public output, logsIt does not remove runtime values
Partial<T>Makes all keys optionalDrafts, PATCH, in-progress formsIt is shallow
Required<T>Makes all keys requiredValidated data before savingIt may require too much
Readonly<T>Prevents reassignmentSettings, permissions, constantsNested objects need separate care
Record<K, T>Builds a known-key dictionaryRole permissions, labels, pricesRecord<string, T> is often too broad
ReturnType<T>Gets a function return typeSync API and UI typesUse typeof functionName
Awaited<T>Gets the resolved Promise valueAsync function result typesIt 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.

#Claude Code #TypeScript #utility types #type safety #code quality
Free

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.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.