惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
量子位
博客园 - 司徒正美
IT之家
IT之家
V
Visual Studio Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
Google DeepMind News
Google DeepMind News
Last Week in AI
Last Week in AI
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
aimingoo的专栏
aimingoo的专栏
GbyAI
GbyAI
Vercel News
Vercel News
B
Blog
大猫的无限游戏
大猫的无限游戏
D
DataBreaches.Net
小众软件
小众软件
罗磊的独立博客
博客园 - 叶小钗
雷峰网
雷峰网
Martin Fowler
Martin Fowler
Hugging Face - Blog
Hugging Face - Blog
WordPress大学
WordPress大学

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
TypeScript Utility Types: How They Work (Not Just What Th...
Kai Thorne · 2026-06-15 · via DEV Community

Every TypeScript developer uses Pick, Omit, Partial, and Record. But ask them how Omit is actually defined, and you'll get blank stares half the time.

That's not a criticism — it means the abstractions are working. But when you understand how these types are built, three things happen:

  1. You stop guessing which utility to use
  2. You can combine them to solve real problems
  3. You can write your own when the built-ins aren't enough

Let's walk through every built-in utility type from the inside out.


The Engine: Mapped Types and Conditional Types

Before we touch a single utility, you need two mental models.

Mapped types transform an object type by iterating over its keys:

// Takes an object type T, returns a new type with same keys but values as strings
type Stringify<T> = {
  [K in keyof T]: string
}

// Usage:
type User = { id: number; name: string; email: string }
type StringUser = Stringify<User>
// { id: string; name: string; email: string }

Conditional types select between two types based on a condition:

type IsString<T> = T extends string ? true : false

type A = IsString<"hello"> // true
type B = IsString<42>      // false

Every utility type in this article is built from these two primitives (plus keyof, typeof, and indexing). No magic.


1. Partial<T> — Make Every Property Optional

What it does: Takes a type and returns a new type where every property is optional.

How it works:

// Real definition in lib.es5.d.ts:
type Partial<T> = {
  [P in keyof T]?: T[P]
}

The ? modifier is what makes each property optional. That's it — a single mapped type with an optional marker.

Real-world use:

interface UserConfig {
  theme: "light" | "dark"
  fontSize: number
  notifications: boolean
}

function applyConfig(updates: Partial<UserConfig>) {
  // Merge incoming partial updates with current config
  return { ...currentConfig, ...updates }
}

// Usage:
applyConfig({ theme: "dark" })
applyConfig({ fontSize: 14, notifications: false })
applyConfig({}) // Also valid — no changes

This is the most common argument type for PATCH endpoints and setState-style updates.


2. Required<T> — Make Every Property Mandatory

What it does: The inverse of Partial. Every optional property becomes required.

How it works:

type Required<T> = {
  [P in keyof T]-?: T[P]
}

The -? syntax removes the optional modifier. The - prefix strips modifiers instead of adding them.

Real-world use:

interface DraftPost {
  title?: string
  body?: string
  tags?: string[]
}

function publishPost(post: Required<DraftPost>) {
  // All fields must be filled in before publishing
  // publishPost({ title: "Hi" }) // ❌ Error — body and tags required
}


3. Readonly<T> — Lock Down Properties

What it does: Marks every property as readonly.

How it works:

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

Real-world use:

function freezeConfig<T extends object>(config: T): Readonly<T> {
  return Object.freeze(config)
}

const APP_CONFIG = freezeConfig({
  apiUrl: "https://api.example.com",
  timeout: 5000,
})

// APP_CONFIG.apiUrl = "https://evil.com"  // ❌ Cannot assign to readonly

Combine with Partial for the classic "configuration that can be partially set once" pattern:

type ImmutableConfig<T> = Readonly<Partial<T>>


4. Pick<T, K> — Select Specific Keys

What it does: Creates a type with only the keys you specify from T.

How it works:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}

The constraint K extends keyof T ensures you can only pick keys that actually exist on T. TypeScript catches typos at compile time.

Real-world use:

interface User {
  id: string
  name: string
  email: string
  passwordHash: string
  ssn: string
  role: "admin" | "user"
}

// Public profile — never expose sensitive fields
type PublicUser = Pick<User, "id" | "name" | "email" | "role">

function getPublicProfile(user: User): PublicUser {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    // passwordHash and ssn are simply not returned
  }
}


5. Record<K, T> — Build Object Types from Scratch

What it does: Creates an object type where keys are type K and values are type T.

How it works:

type Record<K extends keyof any, T> = {
  [P in K]: T
}

keyof any is string | number | symbol — the set of valid JavaScript object keys.

Real-world use — mapping enums to data:

type HttpStatus = 200 | 201 | 400 | 401 | 500

type StatusMessages = Record<HttpStatus, string>

const messages: StatusMessages = {
  200: "OK",
  201: "Created",
  400: "Bad Request",
  401: "Unauthorized",
  500: "Internal Server Error",
}

TypeScript will enforce that you include every key in the union:

const badMessages: StatusMessages = {
  200: "OK",
  // ❌ Error: 201, 400, 401, 500 are missing
}

Key pattern — dictionaries:

type UserMap = Record<string, User>

const users: UserMap = {}
users["abc123"] = { id: "abc123", name: "Alice", email: "alice@example.com" }

But Record<string, T> is a dictionary — it accepts any string key. For stricter mappings, use a union type as K.


6. Exclude<T, U> — Remove from a Union

What it works on: Union types (not object types).

How it works:

type Exclude<T, U> = T extends U ? never : T

This distributes over the union T. For each member of T:

  • If it extends U, it becomes never (removed)
  • Otherwise, it stays
type Shape = "circle" | "square" | "triangle" | "rectangle"

// Remove 'triangle' from the union
type Polygon = Exclude<Shape, "triangle">
// "circle" | "square" | "rectangle"

// Remove multiple
type NoCircles = Exclude<Shape, "circle" | "square">
// "triangle" | "rectangle"

The distributive property is key here. Exclude<string | number | boolean, string | number> evaluates as:

(string extends string | number ? never : string)   never
| (number extends string | number ? never : number)  never
| (boolean extends string | number ? never : boolean)  boolean
// Result: boolean


7. Extract<T, U> — Keep Only Matching Union Members

What it does: The inverse of Exclude — keeps only the members of T that are assignable to U.

How it works:

type Extract<T, U> = T extends U ? T : never

Real-world use — event type narrowing:

type AllEvents =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string }
  | { type: "focus" }
  | { type: "blur" }

// Get only the event types that have a specific shape
type ClickEvents = Extract<AllEvents, { type: "click" }>
// { type: "click"; x: number; y: number }

type InputEvents = Extract<AllEvents, { type: "keypress" | "focus" }>
// { type: "keypress"; key: string } | { type: "focus" }


8. Omit<T, K> — Remove Specific Keys

What it does: The inverse of Pick — returns a type with specific keys removed.

How it works:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

This is the most elegant composition of utility types. It:

  1. Gets all keys of T with keyof T
  2. Removes K from that union with Exclude
  3. Picks the remaining keys with Pick

Real-world use — remove internal fields before serialization:

interface InternalTodo {
  id: string
  title: string
  completed: boolean
  _version: number
  _syncStatus: "pending" | "synced"
  _createdBy: string
}

// Public API — strip internal fields
type APITodo = Omit<InternalTodo, "_version" | "_syncStatus" | "_createdBy">
// { id: string; title: string; completed: boolean }

Note: In TypeScript 5.x+, Omit was updated so K no longer needs to extend keyof T — you can pass keys that don't exist on T without error. This makes it safer for generic code where you're not sure which keys exist.


Bonus: The Conditional Utility Types

These use conditional types with infer to extract information from function and constructor types.

Parameters<T> — Get Function Parameter Types

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

function createUser(name: string, age: number, email: string) {
  return { name, age, email }
}

type CreateUserArgs = Parameters<typeof createUser>
// [name: string, age: number, email: string]

ReturnType<T> — Get Function Return Type

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

type CreateUserReturn = ReturnType<typeof createUser>
// { name: string; age: number; email: string }

ConstructorParameters<T> and InstanceType<T>

These work on constructor signatures:

class Database {
  constructor(public host: string, public port: number) {}
}

type DBParams = ConstructorParameters<typeof Database>
// [host: string, port: number]

type DBInstance = InstanceType<typeof Database>
// Database

NonNullable<T> — Remove Null and Undefined

type NonNullable<T> = T extends null | undefined ? never : T

type Maybe = string | null | undefined
type Definitely = NonNullable<Maybe>
// string


Real-World Patterns

Pattern 1: Selective Partial (Deep Partial Update)

The built-in Partial is shallow. For nested updates, you need a recursive version:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

interface NestedConfig {
  server: { host: string; port: number }
  database: { url: string; poolSize: number }
}

function patchConfig(update: DeepPartial<NestedConfig>) {
  // patchConfig({ server: { host: "new-host" } })  // ✅ Works deeply
}

Pattern 2: Pick by Value Type

Sometimes you want to pick keys based on what value type they hold, not by name:

type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K]
}

interface Entity {
  id: string
  name: string
  createdAt: Date
  updatedAt: Date
  metadata: Record<string, unknown>
}

type DateFields = PickByValue<Entity, Date>
// { createdAt: Date; updatedAt: Date }

This uses key remapping via as (TypeScript 4.1+), which lets you filter keys inside the mapped type.

Pattern 3: Make Specific Keys Required

Required<T> makes everything required. What if you only need a subset?

type WithRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

interface Draft {
  title?: string
  body?: string
  tags?: string[]
}

type DraftWithTitle = WithRequired<Draft, "title">
// title is required; body and tags stay optional

Pattern 4: Substitute Property Types

Need to change the type of one property without touching the rest?

type Override<T, R extends Partial<Record<keyof T, unknown>>> = {
  [P in keyof T]: P extends keyof R ? R[P] : T[P]
}

interface Feature {
  name: string
  enabled: boolean
  version: number
}

// Change `version` from number to string
type FeatureUI = Override<Feature, { version: string }>
// { name: string; enabled: boolean; version: string }


Common Gotchas

🚨 Omit with unions behaves unexpectedly

When T is a union, Omit distributes over it:

type Result =
  | { status: "success"; data: string }
  | { status: "error"; message: string }

type WithoutStatus = Omit<Result, "status">
// { data: string } | { message: string }
// Both branches survive — only the status key is removed from each

🚨 Readonly is shallow

interface User {
  name: string
  address: { city: string; zip: string }
}

const user: Readonly<User> = { name: "Alice", address: { city: "NYC", zip: "10001" } }
// user.name = "Bob"         // ❌ Error
// user.address.city = "LA"  // ✅ No error — address is still mutable!

For truly immutable types, you need a recursive DeepReadonly.

🚨 Partial from the right place

If you have a union of objects, Partial distributes:

type State = { type: "loading" } | { type: "loaded"; data: string }
type PartialState = Partial<State>
// { type?: "loading" } | { type?: "loaded"; data?: string }

This is rarely what you want. Map over the union explicitly instead.


Summary

Utility What It Does Built From
Partial<T> Makes all props optional Mapped type with ?
Required<T> Makes all props required Mapped type with -?
Readonly<T> Makes all props readonly Mapped type with readonly
Pick<T, K> Selects specific keys Mapped type constrained to K
Record<K, T> Creates key-value type Mapped type over K
Exclude<T, U> Removes from union Conditional (distributes)
Extract<T, U> Keeps matching union members Conditional (distributes)
Omit<T, K> Removes specific keys Pick + Exclude
Parameters<T> Gets function params infer in conditional
ReturnType<T> Gets function return infer in conditional
NonNullable<T> Removes null/undefined Conditional

The built-in utility types aren't magic — they're just mapped types and conditional types with clear names. Once you understand that, you can read their definitions in your editor (Cmd+Click in VS Code) and even write your own.

And when the built-ins aren't enough, you now have the primitives to build exactly what you need.


Published by Kai Thorne. I write about TypeScript, Python, and developer workflows.