Index Signatures & Record

Difficulty: Intermediate

Question

What are index signatures in TypeScript? How do they compare to the Record utility type?

Answer

Index signatures define the type for properties accessed by a dynamic key. They declare that all properties accessed by a certain key type must have a certain value type.

Syntax: { [key: string]: ValueType }

Record<K, V> is a utility type that creates an object type with keys of type K and values of type V. It is implemented using mapped types and is often preferred over index signatures for clarity.

Key differences: - Index signatures allow any string/number key. Record with a union restricts to specific keys. - Index signatures mix with explicit properties. Record creates a standalone type. - Record<string, V> is equivalent to { [key: string]: V }.

Code examples

Index Signatures

// String index signature
interface Dictionary {
  [key: string]: string;
}
const dict: Dictionary = {
  hello: 'world',
  foo: 'bar',
};
dict['anything'] = 'works'; // any string key is valid

// Mixing index signatures with known properties
interface Config {
  name: string;           // explicit property
  version: number;        // explicit property
  [key: string]: string | number; // all other keys
}
// Note: explicit properties must be assignable to the index signature type

// Number index signature (for array-like objects)
interface StringArray {
  [index: number]: string;
  length: number;
}
const arr: StringArray = { 0: 'a', 1: 'b', length: 2 };

// Readonly index signature
interface ReadonlyMap {
  readonly [key: string]: number;
}
const scores: ReadonlyMap = { alice: 95, bob: 87 };
// scores['alice'] = 100; // Error: readonly

Index signatures handle dynamic keys. When mixing with explicit properties, every explicit property must be assignable to the index signature's value type.

Record Utility Type

// Record with string keys (like index signature)
type Cache = Record<string, unknown>;
const cache: Cache = { user_1: { name: 'Alice' }, count: 42 };

// Record with union keys (exhaustive)
type Theme = 'light' | 'dark' | 'system';
type ThemeConfig = Record<Theme, { bg: string; text: string }>;

const themes: ThemeConfig = {
  light: { bg: '#fff', text: '#000' },
  dark: { bg: '#000', text: '#fff' },
  system: { bg: '#f0f0f0', text: '#333' },
  // Must have ALL three keys - missing one is an error
};

// Record vs index signature
// Index signature: open-ended, any key
interface OpenMap { [key: string]: number; }

// Record with union: closed, only listed keys
type ClosedMap = Record<'a' | 'b' | 'c', number>;
const closed: ClosedMap = { a: 1, b: 2, c: 3 };
// closed.d; // Error: Property 'd' does not exist

// Nested Record
type UserPermissions = Record<string, Record<string, boolean>>;
const perms: UserPermissions = {
  admin: { read: true, write: true, delete: true },
  user: { read: true, write: false, delete: false },
};

Record with union keys creates exhaustive mappings that require all keys. Record with string keys behaves like an index signature but is often more readable.

Practical Dictionary Patterns

// Type-safe dictionary with Map-like interface
interface TypedMap<V> {
  [key: string]: V | undefined; // undefined acknowledges missing keys
}

function getOrDefault<V>(map: TypedMap<V>, key: string, fallback: V): V {
  return map[key] ?? fallback;
}

const ages: TypedMap<number> = { alice: 30, bob: 25 };
const age = getOrDefault(ages, 'charlie', 0); // 0

// Grouping pattern
function groupBy<T>(items: T[], key: keyof T): Record<string, T[]> {
  return items.reduce((groups, item) => {
    const value = String(item[key]);
    (groups[value] ??= []).push(item);
    return groups;
  }, {} as Record<string, T[]>);
}

interface Product { category: string; name: string; price: number; }
const products: Product[] = [
  { category: 'books', name: 'TS Handbook', price: 29 },
  { category: 'books', name: 'Clean Code', price: 35 },
  { category: 'tools', name: 'VS Code', price: 0 },
];
const grouped = groupBy(products, 'category');
// { books: [Product, Product], tools: [Product] }

Adding '| undefined' to index signatures acknowledges that not every key may be present. This is safer than assuming all lookups succeed.

Key points

Concepts covered

Index Signatures, Record Type, Dictionary Patterns, Dynamic Keys, Symbol Keys