TypeScript's built-in utility types transform existing types without rewriting them — Partial makes all fields optional, Pick selects a subset of fields, Omit removes fields, Record defines key-value maps, and Exclude/Extract manipulate union types — all resolved at compile time with zero runtime cost.
TypeScript ships with built-in utility types — Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract — that transform types without you writing the transformation from scratch every time.
Object utility types
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
address?: string;
}
// Partial<T> — all properties become optional
type UserUpdate = Partial<User>;
const update: UserUpdate = { name: "Priya" }; // all other fields omitted
// Required<T> — all optional properties become required
type FullUser = Required<User>; // address is now required
// Readonly<T> — all properties become readonly
type FrozenUser = Readonly<User>;
// frozenUser.name = "x"; // Error
// Pick<T, K> — keep only specified keys
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit<T, K> — remove specified keys
type UserWithoutRole = Omit<User, "role" | "id">;
// { name: string; email: string; address?: string }
// Record<K, V> — object type with keys K and values V
type RolePermissions = Record<"admin" | "user" | "guest", string[]>;
const perms: RolePermissions = {
admin: ["read","write","delete"],
user: ["read","write"],
guest: ["read"],
};
// Record for a lookup table
type ErrorMessages = Record<number, string>;
const httpErrors: ErrorMessages = { 404: "Not Found", 500: "Server Error" };Set and function utility types
// Union manipulation
type AllRoles = "admin" | "user" | "guest" | "moderator";
type PublicRoles = "user" | "guest";
// Exclude<T, U> — remove U from union T
type PrivateRoles = Exclude<AllRoles, PublicRoles>; // "admin" | "moderator"
// Extract<T, U> — keep only the members of T that are in U
type CommonRoles = Extract<AllRoles, "user" | "guest" | "unknown">; // "user" | "guest"
// NonNullable<T> — remove null and undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
// Function utility types
function fetchUser(id: number, token: string): Promise<User> { ... }
type Params = Parameters<typeof fetchUser>; // [number, string]
type Result = ReturnType<typeof fetchUser>; // Promise<User>
type Resolved = Awaited<ReturnType<typeof fetchUser>>; // User
// ConstructorParameters<T> — constructor argument types
class Logger {
constructor(private name: string, private level: number) {}
}
type LoggerArgs = ConstructorParameters<typeof Logger>; // [string, number]
// InstanceType<T> — the type an constructor produces
type LoggerInstance = InstanceType<typeof Logger>; // LoggerAdvanced patterns: satisfies, const, and template literals
// satisfies (TypeScript 4.9) — validate type without widening
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Record<string, string | number[]>;
// satisfies checks the object against Record<string, string | number[]> but keeps
// each property's own precise inferred type, so palette.red stays number[] here —
// same as if the "satisfies ..." clause were deleted and palette were left with no
// annotation at all. The widening only happens if you swap satisfies for a plain
// type annotation instead: `const palette: Record<string, string | number[]> = {...}`
// makes palette.red string | number[], because every property now takes the
// declared type. satisfies gets you the validation without paying that price.
palette.red.map(v => v * 2); // OK — TypeScript knows it's number[]
// as const — make all values literal types, readonly recursively
const config = {
host: "localhost",
port: 3000,
debug: true,
} as const;
// config: { readonly host: "localhost"; readonly port: 3000; readonly debug: true }
// Combined with typeof for enums-without-enums
const Direction = {
NORTH: "north",
SOUTH: "south",
EAST: "east",
WEST: "west",
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];
// "north" | "south" | "east" | "west"
// Template literal types for API path validation
type ApiPath = `/api/${string}`;
function callApi(path: ApiPath) { ... }
callApi("/api/users"); // OK
// callApi("/users"); // Error — must start with /api/tsconfig.json — the critical settings
{
"compilerOptions": {
"target": "ES2022", // JS version to output
"module": "ESNext", // module system (ESNext = ES modules)
"lib": ["ES2022", "DOM"], // type definitions to include
"outDir": "./dist", // where to put compiled JS
"rootDir": "./src", // where source TS lives
// Strictness — always enable these
"strict": true, // enables all strict checks below
"noImplicitAny": true, // error on implicit any
"strictNullChecks": true, // null/undefined are not assignable to other types
"strictFunctionTypes": true, // correct function parameter variance
"noUncheckedIndexedAccess": true, // arr[0] is T | undefined, not T
"noImplicitReturns": true, // all code paths must return
"noFallthroughCasesInSwitch": true, // switch cases must break/return
// Module resolution
"moduleResolution": "bundler", // for Vite/esbuild/Webpack 5
"resolveJsonModule": true,
"esModuleInterop": true,
"isolatedModules": true, // each file compilable independently (needed for esbuild)
// Paths
"paths": {
"@/*": ["./src/*"] // absolute imports like: import { x } from "@/utils"
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Declaration merging and module augmentation
// Add properties to an existing module's types
// Useful for adding types to third-party libraries
// Augment Express Request type
import "express";
declare module "express" {
interface Request {
user?: { id: number; role: string };
}
}
// Now req.user is typed everywhere you use Express
app.get("/profile", (req, res) => {
if (req.user) {
res.json({ id: req.user.id }); // type-safe
}
});
// Global augmentation
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
window.analytics.track("page_view"); // type-safe
// Namespace merging — add to a namespace
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
namespace Validation {
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) { return /^[A-Za-z]+$/.test(s); }
}
}strict checking is on by default has changed over time. For years, a fresh tsconfig.json without an explicit "strict": true defaulted to permissive checking — implicit any allowed, null checks off — which is why older guides always say to add "strict": true yourself. Since TypeScript 6.0, strict-style checking (no implicit any, strict null checks, and the rest of the strict family) is the default even when the flag is left out of tsconfig entirely; you now have to opt out of individual checks rather than opt in. Either way, enabling stricter settings on an existing codebase with loose types will surface many errors — that's correct, not a bug.Partial<T> makes only the top-level properties optional — nested objects are untouched. Partial<{ user: { name: string } }> makes user itself optional, but if user is present it still requires a full { name: string }; name is not made optional too. For deep partial, you need a recursive type: type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }.as const makes values readonly and literal, but does not make objects immutable at runtime. At runtime, the object can still be mutated — as const is a TypeScript compile-time constraint only. It narrows the types to the exact literal values, preventing TypeScript from widening "north" to string.The TypeScript type system: soundness and intentional unsoundness
TypeScript is intentionally not fully sound — it makes pragmatic trade-offs. Known unsoundnesses: (1) function parameter bivariance for methods (for compatibility with common OOP patterns); (2) type assertions (as T) bypass the type checker; (3) any is a hole in the type system; (4) object literals have excess property checking but objects in variables do not; (5) noUncheckedIndexedAccess is not in strict and must be enabled separately. The TypeScript team explicitly documents these trade-offs and refers to them as "unsoundness for practicality."
TypeScript 5.x — new features
TypeScript 5.0 (March 2023) introduced: decorators implementing the TC39 decorators proposal (Stage 3), const type parameters, and --verbatimModuleSyntax for cleaner ESM output. (Two related features are often misdated alongside it: decorator metadata — emitting via Symbol.metadata — didn't arrive until 5.2, and variadic tuple types are older still, from TypeScript 4.0.) TypeScript 5.2 added using declarations (using / await using) implementing the TC39 Explicit Resource Management proposal — the TypeScript equivalent of Java try-with-resources, ensuring disposal even on error. TypeScript 5.5 (June 2024) added inferred type predicates — the compiler can automatically infer user-defined type guards from function bodies without requiring explicit value is T annotations.