Utility type record in TypeScript
Record<K, T> is a TypeScript utility type that creates an object type where every key from union K must be present and maps to type T.
Theory
TL;DR
- Analogy: Record is like a spreadsheet template where column headers are fixed and every cell holds the same data type.
- Main difference from
{ [key: string]: T }: Record forces ALL keys from your union to exist in the object; index signatures accept any string with no enforcement. - Internally,
Record<K, T>compiles to{ [P in K]: T }, which is a mapped type. - Decision rule: use Record when keys are known and finite. Use index signatures when keys are open-ended or dynamic.
Quick example
type Status = "pending" | "approved" | "rejected";
type StatusConfig = Record<Status, { label: string; color: string }>;
const config: StatusConfig = {
pending: { label: "Waiting", color: "#FFA500" },
approved: { label: "Done", color: "#00AA00" },
rejected: { label: "Denied", color: "#FF0000" }
// Drop any key and TypeScript throws an error immediately.
};All three keys must be present. That is the whole contract.
Key difference
Record guarantees exhaustiveness. Define Record<"a" | "b" | "c", T> and TypeScript forces all three keys at compile time. An index signature like { [key: string]: T } accepts any string with no enforcement at all. Record makes contracts explicit. Index signatures offer flexibility.
When to use
- Enum-like configs: role names to permissions, status codes to messages, feature flags to booleans.
- Lookup tables: user IDs to profiles, currency codes to exchange rates.
- State machines: each state mapped to its allowed transitions or handler functions.
- Skip Record when keys are truly dynamic or unbounded. An index signature or
Mapfits better there.
How the compiler handles this
TypeScript treats Record<K, T> as shorthand for { [P in K]: T }. The compiler iterates through each member of union K and creates a required property. Omit any key and it reports an error. At runtime there is zero overhead. Record compiles to a plain JavaScript object.
Common mistakes
Mistake 1: Forgetting a key after extending the union
The most common one I see after code reviews: adding a new value to the union but forgetting to update the config object.
type Permission = "read" | "write" | "delete" | "admin";
type PermissionConfig = Record<Permission, boolean>;
// TypeScript error: property 'admin' is missing
const config: PermissionConfig = {
read: true,
write: true,
delete: true,
};
// Fix: add all keys
const configFixed: PermissionConfig = {
read: true,
write: true,
delete: true,
admin: false,
};Mistake 2: Using Record when keys should be optional
type Feature = "darkMode" | "analytics" | "beta";
// Record requires all keys, so this fails:
const flags: Record<Feature, boolean> = {
darkMode: true,
analytics: true,
// Error: missing "beta"
};
// Fix: wrap with Partial
const flagsFixed: Partial<Record<Feature, boolean>> = {
darkMode: true,
analytics: true, // Valid now
};Mistake 3: Using Record<string, T> when keys are actually known
// This is just an index signature under a different name
type D = Record<string, number>; // Same as { [key: string]: number }
// When keys are known, be explicit
type Currency = "USD" | "EUR" | "GBP";
const rates: Record<Currency, number> = {
USD: 1.0,
EUR: 0.92,
GBP: 0.87, // TypeScript forces all three
};Real-world usage
- React: component registries
Record<ComponentName, ComponentType> - Redux: action type to handler mappings
Record<ActionType, ActionCreator> - Express: HTTP method handlers
Record<"GET" | "POST" | "DELETE", RequestHandler> - Testing: mock data factories
Record<UserRole, MockUser>
Follow-up questions
Q: What is the difference between Record<K, T> and { [P in K]: T }?
A: They produce identical types. Record is syntactic sugar for the mapped type. Use Record for simple key-value contracts. Write the mapped type directly when you need per-key conditional logic, like { [P in K]: P extends "admin" ? AdminConfig : UserConfig }.
Q: How do you make some Record keys optional?
A: Use Partial<Record<K, T>> to make all keys optional. Or use Record<K, T | undefined> if you want every key present but allow undefined values.
Q: When would you pick Map<string, T> over Record<string, T>?
A: Record works best for static, typed configurations that compile to plain objects. Map handles dynamic, unbounded keys and has built-in iteration helpers. For config objects use Record. For caches or user-generated key-value pairs, Map fits better.
Q: What happens if you add an extra key at runtime?
A: JavaScript allows it. Record only enforces the contract at compile time. TypeScript flags config.newKey = "value" as a type error, but the runtime assignment still executes.
Examples
Role permissions map
type UserRoles = "admin" | "user" | "guest";
type RolePermissions = Record<UserRoles, string[]>;
const rolePermissions: RolePermissions = {
admin: ["create", "edit", "delete"],
user: ["view", "edit"],
guest: ["view"],
};
// TypeScript knows this is string[], not string[] | undefined
console.log(rolePermissions["admin"]); // ["create", "edit", "delete"]Every key from UserRoles is required. Add "moderator" to the union and TypeScript immediately asks for a moderator property in the object.
Express route handlers
import { Request, Response } from "express";
type RouteHandlers = Record<
"GET" | "POST" | "DELETE",
(req: Request, res: Response) => void
>;
const handlers: RouteHandlers = {
GET: (req, res) => res.json({ data: [] }),
POST: (req, res) => res.status(201).json({}),
DELETE: (req, res) => res.status(204).send(),
};Adding a new HTTP method to the union means you must implement its handler before the code compiles. The type contract enforces complete coverage.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.