Code Your Path Coding School

Ultimate Guide to TypeScript Record Type — Learn When And How To Use TypeScript Record Type With Examples!

TypeScript Record

Table of Contents

The TypeScript Record type is a versatile and easy way to create type-safe and maintainable data structures.

In this guide, I use Record in five examples to define objects with predetermined keys and values — indispensable for code safety and clarity.

Defining user roles, application settings, and dynamic content in React components — this guide covers a range of the Record type scenarios.

Understanding TypeScript Record Type – 1st Example

The Record type is part of TypeScript’s standard library (defined in the lib.es5.d.ts file) and available as part of the ECMAScript 5 type definitions.

Don’t forget to include es5 in the lib array to ensure that it can use this type.

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"]
  }
}

If the lib array is not set in your tsconfig.json, TypeScript uses a default library that corresponds to the target compiler option.

To see the definition directly in your codebase, you can hover over the word Record in your IDE (if it supports TypeScript).

This should display a tooltip with the type definition, like this:

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Using Record type with a union of specific strings as keys helps structure data, for example, for status management scenarios.

Suppose you are working with an API response that can be in one of three statuses: ‘error’, ‘success’, or ‘pending’.

You should specify what type of data each status holds. In this case, each status has a message and a timestamp, but you can extend this further:

// Define a type for each status
interface StatusInfo {
    message: string;
    timestamp: Date;
}

Then, you create a Type for the variable that will hold the API response. Without the TS Record type, you can use direct interface definition to explicitly define each key (error, success, pending) and strongly type to StatusInfo:

// Define an interface that includes all status keys directly
interface StatusReport {
    error: StatusInfo;
    success: StatusInfo;
    pending: StatusInfo;
}

// Initialize the status report object with specific status data
const statusReport: StatusReport = {
    error: {
        message: "An error!!",
        timestamp: new Date() 
    },
    success: {
        message: "Success!!",
        timestamp: new Date() 
    },
    pending: {
        message: "Pending...",
        timestamp: new Date() 
    }
};

Using the Record type, we can eliminate the bulky Response interface.

The TypeScript Record type lets us create types out of Union types. Define a union type that includes all possible keys (‘error’ | ‘success’ | ‘pending’). 

// Define the union type for the keys
type StatusKeys = 'error' | 'success' | 'pending';

Then, use the Record type utility to map these keys to the StatusInfo interface. We dynamically define a StatusReport type where the Record defines the type of each key-value pair:

// Using Record to map each status to its corresponding data
type StatusReport = Record<StatusKeys, StatusInfo>;

The Record<StatusKeys, StatusInfo> type definition ensures that each property in the statusReport object conforms to the StatusInfo type.

I often use this type since it’s easy to modify the type of keys without changing the interface.

2nd Example Of Record Type in TypeScript

Let’s explore another common application of the TypeScript Record type.

Imagine you’re developing a parking lot app. You need a comprehensive list of all cars in the garage, for which you implement Car and Garage types:

type Car = {
  make: string;
  model: string;
  year: number;
};

type Garage = { [key: string]: Car }

Below, we create a simple garage object where the key is a string (“car1” and “car2“), and the value is of type Car:

const garage: Garage = {
  car1: {
    make: "Toyota",
    model: "Corolla",
    year: 2020
  },
  car2: {
    make: "Honda",
    model: "Civic",
    year: 2018
  }
};

To enhance readability, we can use the Record Type for the Garage type:

type Garage = Record<string, Car>

A Record type maps a string key to Car objects. We define each car object by its predefined properties: make, model, and year.

Again, to me, the type Garage = Record<string, Car> looks so much neater and far more readable than the type Garage = {[key: string]: Car}.

Both describe the same object type — the first is a more explicit definition, while the other uses the utility type Record for concision.

Example 3: Dynamic Types with Record and Abstract Classes

TypeScript’s Record type and abstract classes allow for robust type checking.

Consider an abstract Model class, which serves as the parent for any other model:

abstract class Model {
    abstract display(): string;
}

Next, I define various implementations of this model, each having its unique collection of methods:

class A extends Model {
    foo() { return 'foo'; }
    display() { return this.foo(); }
}

class B extends Model {
    bar() { return 'bar'; }
    display() { return this.bar(); }
}

Now, we can then create a record that holds instances of these classes:

const record: Record<string, Model> = {
    classA: new A(),
    classB: new B(),
};

In this setup, the properties of the record are dynamic, meaning each could be an instance of a different subclass of the Model.

You can access these models via the record leveraging the dynamic nature of JavaScript while still enjoying the benefits of TypeScript’s static type checking:

console.log(record.classA.display());  // Outputs 'foo'
console.log(record.classB.display());  // Outputs 'bar'

TypeScript’s Record type, in combination with abstract classes, allows highly flexible and type-safe data structures. I find this method especially useful when I handle types through a uniform interface.

Example 4 – Defining Data Structure with Known Required Fields

When defining data structures in TypeScript, you need an object that includes both static and dynamic properties.

One common scenario is having an object where most keys are dynamic but some (such as “meta”) must always present and have a predefined structure.

Consider a product object that always includes a properties field with dynamic key-value pairs but also contains a meta field with specific attributes (like id and type):

const product = {
  properties: {
    color: "red",
    meta: {
      id: 1,

      type: "car",
    }
  }
};

To properly define this structure in TypeScript using the Record type and an interface, first define the meta field’s structure:

interface MetaProps {
  type: "meta"; // Replace with a union for future extensibility
  id: number;
  // Add additional fields here
}

Then, combine this with the Record type for the rest of the dynamic properties (string keys with string | number values):

type Props = { meta: MetaProps } & Record<string, string | number>;

This Props type requires that meta field of the MetaProps type, and any other properties of a type are either strings or numbers.

Here’s how you annotate the product object to enforce the Props structure:

let product: { properties: Props } = {
  properties: {
    color: "red",
    meta: {
      type: "car",
      id: 1
    }
  }
};

While you might use only the Record type, it wouldn’t guarantee the presence of meta field. The combination of { meta: MetaProps } & Record<string, string | number> states that meta must exist and match MetaProps, while still allowing flexibility for other dynamic keys.

Example 5 – TS Record Type in a React Example

Let’s implement a React component that displays status icons dynamically of our application.

First, we define a union type listing all possible states:

type Statuses = "failed" | "complete";

For such a union, we can use a Record type to map each status with its corresponding icon properties.

type IconTypes = "warning" | "check";  // Example icon types
type IconColors = "red" | "green";    // Example icon colors

const icons: Record<Statuses, { iconType: IconTypes; iconColor: IconColors }> = {
  failed: { iconType: "warning", iconColor: "red" },
  complete: { iconType: "check", iconColor: "green" }
};

I can now create a functional React component that uses these mappings to render the appropriate icon depending on the current status:

import React from 'react';
import Icon from './Icon'; // Assuming an Icon component is available

interface StatusProps {
  status: Statuses;
}

const Status: React.FC<StatusProps> = ({ status }) => (
  <Icon {...icons[status]} />
);

export default Status;

The Status component takes a status prop, looks up the corresponding icon settings in the icons object, and renders an Icon component with the appropriate properties spread into it.

Using TypeScript Record type in this example provides multiple benefits:

  1. Type Safety. When we update our Statuses union (e.g., adding a new status), TypeScript will immediately flag errors where the icons mapping is incomplete, prompting a quick fix.
  2. Maintainability. I use this pattern when the application has many types, and I need to refer to these types in multiple places. This makes maintenance straightforward and less error-prone.
  3. Scalability. Adding new statuses (and corresponding icons) is just a matter of extending the Statuses union and the icons record.

Record type in React ensures components behave correctly in different states and prevent runtime errors, creating dynamic and responsive UIs.

Time Complexity Of Accessing Record Key

TypeScript Record type is not a runtime entity but a TypeScript compile-time abstraction that translates directly to a plain JavaScript object. Thus, the time complexity of accessing keys in a Record is the same as accessing properties in a JavaScript object.

For JavaScript objects (especially with a stable set of properties), modern JavaScript engines (V8 in Chrome and Node.js) optimize property access to be O(1) (constant) time.

V8 treats these objects like instances of runtime-generated classes with static slots for properties. Accessing any property is incredibly efficient as long as the object’s shape (its set of properties) does not change.

Difference between Record and Object Types

Difference between Record and Object Types

The syntax { [k: string]: unknown; } defines an object with an index signature.

This is a traditional JavaScript pattern for creating objects with string keys and values of any type — unknown in this case (which is a safer alternative to any since it requires type checking before performing any operations on the values).

TypeScript Record type simplifies the creation of object types whose property keys are specific sets of keys, each of a certain type. For example:

const roles1: Record<string, string> = {
  alice: "admin",
  bob: "user"
};

This means a Record<string, string> is effectively a shorthand for an index signature { [key: string]: string }

// Example of an index signature

const roles2: { [key: string]: string } = {
  alice: "admin",
  bob: "user"
};

In this use case, roles1 and roles2 are objects with string keys and values, and there is no practical difference in how you use them. 

However,  TypeScript does not allow literal types (like ‘alice’ | ‘bob’) as keys in an index signature — you would need to use a mapped type, which is what Record is doing for you.

While both Record and Object types with index signatures seem functionally equivalent, their usage conveys different intentions:

  • Record: Way more readable and succinct, especially when you know the types of props. I use it when I do not know the exact keys but know the types of keys and values ahead of time.
  • Object with Index Signature: Useful for more complex (nested) structures, conditional and recursive types or other advanced patterns that Record do not support.

After all, both approaches achieve similar outcomes but TypeScript Record provides a cleaner and more direct declaration.

Record vs. Map in TypeScript

Developers often debate the choice between TypeScript Map and Record.

Both serve similar purposes but have distinct characteristics that may make one more suitable than the other, depending on the context.

We use a TypeScript Record to construct an object type whose keys are known and pre-defined, each mapping to a type.

Records are essentially plain JavaScript objects that you create using it as a shorthand for { [P in K]: T } where P must be a string (or a symbol).

They provide a convenient way to assert that every key exists, simplifying code when keys are guaranteed not to be missing.

const record: Record<string, string[]> = {
    key1: ['item1', 'item2'],
    key2: ['item3']
};

Map, an ES6 feature, provides key-value storage (resembles associative arrays). It allows keys of any type and maintains the order of elements, offering operations like set, get, and has.

const map = new Map<string, string[]>();
map.set('key1', ['item1', 'item2']);
map.set('key2', ['item3']);

Key Differences Between Record and Map

Key Differences Between Record and Map

Record Map
Keys Types Keys must be strings or symbols. TS coerces Other types to string Use any key, including objects, numbers, and symbols
Iteration Iterates in an arbitrary order Iterates entries, keys, and values in the order they were inserted
Memory and Performance Offer better performance for frequent access and modifications (as it is a simple object!) Better optimization for frequent additions and deletions of the props
Prototype Pollution Susceptible to prototype pollution if you poorly sanitize keys Safer against prototype pollution

I use Record more for static, defined data structures where TS knows keys at compile time.

Conversely, Map offers more flexibility and safety for dynamic and diverse data types,  ideal for complex collections of data items.

Benefits of Using TypeScript Record Type For  A Software Developer

When you can just use Record, I’d recommend using that in your code. I need to know much less TypeScript concepts using Record — you only need generics (Read my blog post with TS Generics Tutorial).

Although “record” is an overloaded term, it’s relatively easy to grasp and give you great benefits.

Strong Type Safety And Codebase Clarity

The TypeScript Record utility types create structured and type-safe relationships — handy when you want each key in a union to correlate with specific attributes inside an object.

To briefly illustrate, let’s consider a scenario where we define a union of car model names:

type CarModels = "sedan" | "suv" | "convertible";

Using this union, we construct a record where each car model name serves as a key, and the value is an object containing details about the car, such as its price:

type CarList = Record<CarModels, { price: number }>;

To meet the requirements of the CarList type, we need to construct an object strictly to the union of car model names:

const cars: CarList = {
  sedan: { price: 20000 },
  suv: { price: 25000 },
  convertible: { price: 30000 }
};

This setup ensures strong type safety:

  • Missing Models: TypeScript will generate an error if a model name is omitted from the CarList.
  • Invalid Models: Adding a model not included in CarModels will trigger a TypeScript error.
  • Modifications: Any changes to CarModels will prompt TypeScript to re-verify the object keys, which is extremely useful when CarModels is likely to be imported from another file and used in multiple places across your application.

Conclusion: TypeScript Record Utility Type

Integrate Record into your TypeScript projects not only to well-organize your type-safe data but also to produce reliable and scalable code.

Remember, the choice between Record and other types (Indexed objects or Map) depends on your use case. However, TypeScript Record is usually type safer and simpler.

Keep experimenting and adapt examples to your needs! Happy coding!

Share the Post:
Join Our Newsletter