Question

Understanding TypeScript Record<K, T>: Definition, Usage, and Examples

typescripttypescript2.0

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 type T?
  • What role does K play exactly?
  • Does K prevent additional keys that are not listed in K, 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 type T."

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 K must 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 in K
  • 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.

  • K is the list of labels to print on the boxes
  • T is 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.

Take Quiz

Syntax and Examples

Core syntax

Record<K, T>
  • K = the property names
  • T = 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 firstName exist? yes
  • is firstName a string? yes
  • does lastName exist? yes
  • is lastName a 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.

Take Quiz

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:

  • port is 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

ApproachExampleBest when
Record`Record<'a''b', number>`
Manual object type{ a: number; b: number }keys are few or value types differ

Record vs index signature

FeatureRecord<'a' | 'b', number>{ [key: string]: number }
Key set known ahead of timeYesNo
Requires specific keysYesNo
Allows arbitrary string keysNot by defaultYes
Good for fixed mappingsYesSometimes

Example:

type Fixed = Record<'x' | 'y', number>;
type AnyKeys = { [key: string]: number };

Record vs Map

FeatureRecordMap
TypeScript utility typeYesNo
JavaScript runtime objectPlain objectMap instance
Best for JSON-like dataYesLess common
Best for dynamic insertion and special key behaviorNoYes

Record vs Partial<Record<K, T>>

TypeMeaning
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>>;
Take Quiz

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 = keys
  • T = value type
  • keys in K are required
  • direct object literals usually reject extra keys
  • variables with extra keys may still be assignable
  • Record is 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 Map behavior 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.

Take Quiz

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 Record type 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.
Take Quiz

Keep learning

Related questions

View more