Question
Understanding TypeScript Record<K, T>: Definition, Usage, and Examples
Question
In TypeScript, what does Record<K, T> mean?
TypeScript 2.1 introduced the Record utility type. One example often shown is:
function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>
The Advanced Types documentation also shows Record as a mapped type:
type Record<K extends string, T> = {
[P in K]: T;
}
Unlike Readonly, Partial, and Pick, Record is not homomorphic because it does not copy properties from an existing type. For example:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
Based on this, I want to understand the following clearly:
- What is a simple definition of
Record? - Does
Record<K, T>mean that all properties on the object have typeT? - What role does
Kplay exactly? - Does
Kprevent additional keys that are not listed inK, or can other keys still exist? - Is this type:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
exactly the same as this?
type ThreeStringProps = {
prop1: string;
prop2: string;
prop3: string;
}
Short Answer
By the end of this page, you will understand that Record<K, T> is a TypeScript utility type for building an object type whose keys come from K and whose values all have type T. You will also see how it compares to writing object types by hand, how extra keys behave, and when Record is useful in real code.
Concept
Record<K, T> is a utility type that creates an object type.
Its meaning is:
- use the set of property names in
K - for each of those property names, assign the value type
T
The common mental reading is:
"An object with keys
K, where every listed key stores a value of typeT."
The core definition is:
type Record<K extends keyof any, T> = {
[P in K]: T;
}
A simpler form is often shown as:
type Record<K extends string, T> = {
[P in K]: T;
}
What K does
K is the set of allowed property names that this type will create.
For example:
type UserRoles = Record<'admin' | 'editor' | 'viewer', boolean>
This becomes:
type UserRoles = {
admin: boolean;
editor: boolean;
viewer: boolean;
}
So K is not the value type. It is the list of keys.
What T does
T is the type of the value for each key.
In this example:
type Scores = Record<'math' | 'science', number>
both math and science must contain numbers.
Is it the same as writing the object type manually?
Yes, in your example it is effectively the same:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
is equivalent to:
type ThreeStringProps = {
prop1: string;
prop2: string;
prop3: string;
}
Record is just the reusable generic form.
Does it mean all properties on the object are type T?
Not exactly.
More precisely:
- all keys listed in
Kmust exist - each of those keys must have value type
T
It does not mean every possible property everywhere must be of type T.
Does K forbid extra keys?
At the type level, Record<'a' | 'b', number> describes an object that has at least a and b as number properties.
When assigning an object literal directly, TypeScript usually performs excess property checks, so extra keys may be rejected:
type AB = Record<'a' | 'b', number>;
const x: AB = {
a: 1,
b: 2,
c: 3 // error in a direct object literal
};
But if the value comes from somewhere else, extra properties can still exist structurally:
type AB = Record<'a' | 'b', number>;
const source = { a: 1, b: 2, c: 3 };
const x: AB = source; // usually allowed
So the safest beginner rule is:
Record<K, T>requires the keys inK- direct object literals usually cannot include extra keys
- structurally, values with extra properties can still be assignable in some situations
This matters because TypeScript uses structural typing, not exact object matching in all cases.
Why Record matters
Record is useful when:
- you know the possible keys ahead of time
- all those keys should store the same kind of value
- you want a concise reusable type instead of writing many properties manually
This is common for:
- lookup tables
- configuration objects
- mappings from status names to labels
- dictionaries with known keys
Mental Model
Think of Record<K, T> like making a row of labeled storage boxes.
Kis the list of labels to print on the boxesTis the kind of item each box is allowed to hold
Example:
Record<'name' | 'email' | 'city', string>
This means:
- make a box labeled
name - make a box labeled
email - make a box labeled
city - every box must contain a
string
So it is like a template for an object shape.
If you try to put a number into one of those boxes, TypeScript complains. If you forget one of the required boxes, TypeScript complains. If you add an unexpected box in a direct object literal, TypeScript often complains too.
Syntax and Examples
Core syntax
Record<K, T>
K= the property namesT= the type for each property value
Basic example
type Settings = Record<'theme' | 'language', string>;
const appSettings: Settings = {
theme: 'dark',
language: 'en'
};
This is the same as:
type Settings = {
theme: string;
language: string;
};
Number values example
type Scores = Record<'alice' | 'bob', number>;
const scores: Scores = {
alice: 95,
bob: 88
};
Using Record with a union of keys
type Status = 'pending' | 'success' | 'error';
type StatusMessages = Record<Status, string>;
const messages: StatusMessages = {
pending: 'Please wait...',
success: 'Done!',
error: 'Something went wrong.'
};
This is a very common pattern.
Invalid example: missing a key
type Status = 'pending' | 'success' | 'error';
type StatusMessages = Record<Status, string>;
const messages: StatusMessages = {
pending: 'Please wait...',
success: 'Done!'
// error: missing 'error'
};
Invalid example: wrong value type
type Flags = Record<'isAdmin' | 'isActive', boolean>;
const flags: Flags = {
isAdmin: true,
isActive: 'yes' // error: should be boolean
};
Function example
function uppercaseValues<K extends string>(obj: Record<K, string>): Record<K, string> {
const result = {} as Record<K, string>;
for (const key in obj) {
result[key] = obj[key].toUpperCase();
}
return result;
}
const user = {
firstName: 'sam',
lastName: 'lee'
};
const uppercased = uppercaseValues(user);
This function accepts an object whose values are all strings and returns a new object with the same keys and string values.
Step by Step Execution
Consider this example:
type Fields = Record<'firstName' | 'lastName', string>;
const person: Fields = {
firstName: 'Ada',
lastName: 'Lovelace'
};
Step 1: Evaluate the keys
K is:
'firstName' | 'lastName'
That means the object type must include these two properties.
Step 2: Evaluate the value type
T is:
string
That means each property created from K must have a string value.
Step 3: Expand the mapped type mentally
TypeScript treats this roughly like:
type Fields = {
firstName: string;
lastName: string;
}
Step 4: Check the object assignment
const person: Fields = {
firstName: 'Ada',
lastName: 'Lovelace'
};
TypeScript checks:
- does
firstNameexist? yes - is
firstNamea string? yes - does
lastNameexist? yes - is
lastNamea string? yes
So the assignment is valid.
What if something is wrong?
Missing property
const person: Fields = {
firstName: 'Ada'
};
TypeScript reports an error because lastName is missing.
Wrong value type
const person: Fields = {
firstName: 'Ada',
lastName: 123
};
TypeScript reports an error because lastName must be a string.
Extra property in a direct object literal
const person: Fields = {
firstName: 'Ada',
lastName: 'Lovelace',
age: 36
};
TypeScript usually reports an excess property error for age in this direct assignment.
Real World Use Cases
Lookup tables
type Route = 'home' | 'about' | 'contact';
const paths: Record<Route, string> = {
home: '/',
about: '/about',
contact: '/contact'
};
Useful when an app has a fixed set of route names.
Status labels
type Status = 'idle' | 'loading' | 'success' | 'error';
const labels: Record<Status, string> = {
idle: 'Idle',
loading: 'Loading...',
success: 'Success',
error: 'Error'
};
This ensures every status has a label.
Feature flags
type Feature = 'chat' | 'payments' | 'analytics';
const enabled: Record<Feature, boolean> = {
chat: true,
payments: false,
analytics: true
};
Useful for configuration and deployment settings.
API field mappings
type Field = 'first_name' | 'last_name' | 'email';
const fieldMap: Record<Field, string> = {
first_name: 'firstName',
last_name: 'lastName',
email: 'emailAddress'
};
Useful when transforming server data into frontend data.
Validation errors by field
type FormField = 'username' | 'password';
const errors: Record<FormField, string> = {
username: 'Required',
password: 'Too short'
};
Useful when each form field should have a consistent error entry.
Real Codebase Usage
In real TypeScript projects, Record is often used when developers want a key-to-value mapping with known keys.
Common patterns
Configuration objects
type Environment = 'development' | 'production';
const apiBaseUrl: Record<Environment, string> = {
development: 'http://localhost:3000',
production: 'https://api.example.com'
};
Enum-like mappings
type Role = 'admin' | 'user' | 'guest';
const permissions: Record<Role, number> = {
admin: 3,
user: 2,
guest: 1
};
Validation result storage
type Field = 'email' | 'password';
function validate(values: Record<Field, string>): Record<Field, string> {
return {
email: values.email.includes('@') ? '' : 'Invalid email',
password: values.password.length >= 8 ? '' : 'Password too short'
};
}
Transforming values while keeping keys
function mapValues<K extends string, T, U>(obj: Record<K, T>, fn: (value: T) => U): Record<K, U> {
const result = {} as Record<K, U>;
for (const key in obj) {
result[key] = fn(obj[key]);
}
return result;
}
This pattern is common in utility functions.
Why teams like Record
- it is shorter than writing many properties by hand
- it keeps key sets consistent
- it works well with unions such as
'a' | 'b' | 'c' - it improves autocomplete and compile-time safety
Related practical pattern: combining with Partial
Sometimes not all keys are required yet:
type Field = 'name' | 'email' | 'phone';
type DraftErrors = Partial<Record<Field, string>>;
This means:
- the possible keys are
name,email,phone - each value is a string
- but each property is optional
This combination appears often in forms and API response handling.
Common Mistakes
1. Thinking Record<K, T> means any string key
Broken assumption:
type Person = Record<'name' | 'email', string>;
This does not mean all string keys are allowed. It specifically creates name and email.
To represent arbitrary string keys, use:
type StringMap = Record<string, string>;
2. Forgetting required keys
Broken code:
type Config = Record<'host' | 'port', string>;
const config: Config = {
host: 'localhost'
};
Problem:
portis required but missing
Fix:
const config: Config = {
host: 'localhost',
port: '3000'
};
3. Giving the wrong value type
Broken code:
type Flags = Record<'darkMode' | 'beta', boolean>;
const flags: Flags = {
darkMode: true,
beta: 'false'
};
Problem:
'false'is a string, not a boolean
Fix:
const flags: Flags = {
darkMode: true,
beta: false
};
4. Assuming it always blocks extra properties everywhere
Broken assumption:
type AB = Record<'a' | 'b', number>;
const source = { a: 1, b: 2, c: 3 };
const value: AB = source;
Some beginners expect this to fail, but it may be allowed because TypeScript uses structural typing.
Remember:
- direct object literals get stricter excess-property checks
- variables may still be assignable if required properties match
5. Using Record when each property has a different type
Broken fit:
type User = Record<'name' | 'age', string>;
This forces both properties to be strings, which is wrong if age should be a number.
Fix:
type User = {
name: string;
age: number;
};
Use Record when all listed keys share the same value type.
6. Confusing Record with Map
Record is a TypeScript type utility for plain objects.
It is not the JavaScript Map class.
const x: Record<string, number> = { a: 1 };
This is just a normal object, not a Map instance.
Comparisons
Record vs manually written object type
| Approach | Example | Best when |
|---|---|---|
Record | `Record<'a' | 'b', number>` |
| Manual object type | { a: number; b: number } | keys are few or value types differ |
Record vs index signature
| Feature | Record<'a' | 'b', number> | { [key: string]: number } |
|---|---|---|
| Key set known ahead of time | Yes | No |
| Requires specific keys | Yes | No |
| Allows arbitrary string keys | Not by default | Yes |
| Good for fixed mappings | Yes | Sometimes |
Example:
type Fixed = Record<'x' | 'y', number>;
type AnyKeys = { [key: string]: number };
Record vs Map
| Feature | Record | Map |
|---|---|---|
| TypeScript utility type | Yes | No |
| JavaScript runtime object | Plain object | Map instance |
| Best for JSON-like data | Yes | Less common |
| Best for dynamic insertion and special key behavior | No | Yes |
Record vs Partial<Record<K, T>>
| Type | Meaning |
|---|---|
Record<K, T> | all keys in K are required |
Partial<Record<K, T>> | keys in K are optional |
Example:
type RequiredErrors = Record<'email' | 'password', string>;
type OptionalErrors = Partial<Record<'email' | 'password', string>>;
Cheat Sheet
Quick definition
type X = Record<K, T>
Creates an object type with:
- keys from
K - values of type
T
Expansion rule
Record<'a' | 'b', number>
becomes:
{
a: number;
b: number;
}
When to use it
Use Record when:
- you know the keys ahead of time
- all those keys have the same value type
Common examples
type Labels = Record<'save' | 'cancel', string>;
type Flags = Record<'darkMode' | 'beta', boolean>;
type Scores = Record<'alice' | 'bob', number>;
Important rules
K= keysT= value type- keys in
Kare required - direct object literals usually reject extra keys
- variables with extra keys may still be assignable
Recordis equivalent to a mapped type
Related patterns
Partial<Record<K, T>> // optional keys
Readonly<Record<K, T>> // read-only keys
Record<string, T> // arbitrary string keys
Record<number, T> // number keys
Not a good fit when
- different keys need different value types
- you need runtime
Mapbehavior instead of a plain object
FAQ
What is Record<K, T> in TypeScript?
It is a utility type that creates an object type with keys from K and values of type T.
Is Record<'a' | 'b', string> the same as { a: string; b: string }?
Yes. In practice, those two types are equivalent.
What does the K in Record<K, T> represent?
K represents the property names, usually as a union such as 'a' | 'b' | 'c'.
Does Record allow extra properties?
Direct object literals usually get an error for extra properties. But values stored in variables may still be assignable if they include the required keys.
Does Record<K, T> mean every property on the object must be type T?
It means every property listed in K must have type T. It does not globally describe every possible property in every situation.
When should I use Record instead of a normal object type?
Use Record when multiple known keys share the same value type. Use a normal object type when properties have different value types.
Is Record the same as JavaScript Map?
No. Record is a TypeScript type utility for plain objects. Map is a JavaScript runtime class.
Can Record use keys other than strings?
Yes. In modern TypeScript, keys can be string, number, or symbol-like property keys.
Mini Project
Description
Create a small status-message dictionary for an application. This project shows how Record can guarantee that every allowed status has a message and that every message has the same value type.
Goal
Build a type-safe object that maps app statuses to user-facing messages using Record.
Requirements
- Define a union type for the allowed statuses.
- Create a
Recordtype that maps each status to a string message. - Create an object that satisfies the type.
- Write a function that accepts a status and returns the corresponding message.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.