Index Signatures in TypeScript: Typing Objects with Dynamic Property Names
Introduction
Objects with dynamic keys are everywhere in JavaScript and TypeScript: configs keyed by id, dictionaries loaded from APIs, memo caches, and style maps. While JavaScript treats object keys as free-form strings, TypeScript gives us tools to describe that structure precisely so we keep type safety without losing flexibility. This article explores index signatures, a core TypeScript feature for typing objects with dynamic property names. You'll learn how to write index signatures, how they interact with other TypeScript features, and how to avoid common pitfalls when mixing dynamic keys with stricter typed properties.
In this tutorial you will learn practical patterns for simple and nested index signatures, how to combine them with utility types, how to leverage generics and mapped types to create type-safe dictionaries, and how to narrow and validate access to dynamic properties at runtime. The examples are focused on intermediate developers and provide step-by-step guidance, code snippets, and troubleshooting tips you can apply immediately.
By the end of the article you will know how to model flexible objects without losing autocompletion, how to avoid accidental any types, and how to migrate code that relies on dynamic keys toward safer patterns. We will also link to related topics like utility types, generics, and narrowing techniques when relevant so you can deepen your knowledge further.
Background & Context
Index signatures let you tell the TypeScript compiler what types of values are stored under keys whose names you don't know ahead of time. Typical forms are string index signatures and number index signatures. A common use case is a lookup table keyed by id or name where keys are created dynamically at runtime.
Index signatures are important because they balance flexibility and safety. Without them, developers often resort to 'any' or to skipping type checks, which defeats TypeScript's purpose. With index signatures you can ensure all values follow the same shape and still allow unknown keys. This becomes more powerful when combined with utility types such as Partial, Pick, or advanced features like generics and mapped types.
If you are already comfortable with TypeScript basics, this guide will extend your toolkit and point you to related topics such as utility types and type narrowing. For an overview of utility types that pair nicely with index signatures, see our introduction to utility types Introduction to Utility Types: Transforming Existing Types.
Key Takeaways
- Index signatures describe objects with dynamic property names and uniform value types.
- Use string and number index signatures to reflect runtime key types precisely.
- Combine index signatures with Readonly, Partial, Pick, and Omit to shape flexible APIs.
- Use generics and mapped types to create typed dictionaries with constrained key unions.
- Apply narrowing and runtime checks to safely access dynamic keys.
- Avoid conflicting explicit properties and broad index signatures to keep type safety.
Prerequisites & Setup
This tutorial assumes intermediate familiarity with TypeScript, including interfaces, types, and basic generics. To follow along, install Node and TypeScript locally or use an editor like VS Code with the TypeScript language service. Initialize a project with:
npm init -y npm install -D typescript npx tsc --init
You can run examples using ts-node or compile and run with node. For more on generics and reusable patterns that pair well with index signatures, see our guide on Introduction to Generics and on Generic Interfaces.
Main Tutorial Sections
What is an index signature?
An index signature lets you describe the type of values accessed via arbitrary keys. The simplest form is a string index signature:
interface StringMap {
[key: string]: number
}
const counts: StringMap = {
apples: 10,
bananas: 5
}
const value = counts['apples'] // inferred as numberThis tells the compiler every property with a string key has a number value. Use this when values are homogenous and keys are dynamic. Index signatures do not restrict which keys exist; they constrain the types of values stored under any key.
String index vs number index
TypeScript treats string and number index signatures differently. A number index signature means property access via numeric keys (or array-like objects). Note that number keys are converted to strings when used in JS objects, but TypeScript offers distinct types:
interface NumMap {
[index: number]: string
}
const arrLike: NumMap = ['a', 'b']
const s = arrLike[0] // stringIf you supply both a string and a number index signature, the value type for number must be a subtype of the string index value type. This is because numeric accesses are also valid string keys at runtime.
Mixing explicit properties and index signatures
You can mix explicit properties with an index signature, but watch compatibility. For example:
interface Config {
version: number
[key: string]: string | number
}
const cfg: Config = { version: 1, name: 'app' }Here version is number and the index signature allows string | number. If you try to make the index signature narrower than an explicit property type, TypeScript will error. Prefer union types on the index signature to accommodate explicit props.
Readonly and index signatures
Often you want dictionaries to be immutable. Combine Readonly with index signatures to enforce immutability:
interface ReadonlyMap {
readonly [key: string]: number
}
const fixed: ReadonlyMap = { a: 1 }
// fixed.a = 2 // error: cannot assign to 'a'For more on immutability patterns and Readonly utility, see Using Readonly
Optional properties vs index signatures
Partial and optional properties have overlapping use cases with index signatures. Partial
type Known = Partial<{ a: number; b: string }>
// vs
interface Dict { [key: string]: number }If you need optional dynamic entries, consider Record plus Partial or make the value type optional. For guidelines on Partial, see Using Partial
Using Record for clean index-style types
Record lets you build index-like types from known key unions:
type UserRole = 'admin' | 'editor' | 'viewer'
type RoleSettings = Record<UserRole, { canEdit: boolean }>
const defaults: RoleSettings = {
admin: { canEdit: true },
editor: { canEdit: true },
viewer: { canEdit: false }
}Record is a great alternative when you do know the set of keys but still want a uniform value type. It interacts nicely with utility types such as Pick and Omit, which are discussed in Using Pick<T, K>: Selecting a Subset of Properties and Using Omit<T, K>: Excluding Properties from a Type.
Generics and index signatures for typed dictionaries
Generics let you write reusable dictionary types that stay type-safe:
interface Dict<K extends string | number, V> {
[key: string]: V // implementation uses string, but K constraints help API
}
function getOrDefault<V>(dict: Record<string, V>, key: string, def: V): V {
return key in dict ? dict[key] : def
}If you want keyed access with a discrete key union, combine generics with Record and constrained key types. For more patterns on generics, review Generic Functions: Typing Functions with Type Variables and Generic Interfaces: Creating Flexible Type Definitions.
Narrowing dynamic property access safely
Accessing a dynamic property often returns a union or any. Use runtime guards and TypeScript narrowing to make access safe:
type Data = { [key: string]: string | number }
function readString(data: Data, key: string): string | undefined {
const val = data[key]
if (typeof val === 'string') {
return val
}
return undefined
}This is a natural place to apply techniques from Type Narrowing with typeof Checks in TypeScript and the in operator patterns described in Type Narrowing with the in Operator in TypeScript.
Using mapped types for advanced transformations
Mapped types let you transform property types across a set of keys. Combine index-like patterns with mapped types to produce derived dictionaries:
type Keys = 'one' | 'two'
type Wrapped<T> = { [K in keyof T]: { value: T[K] } }
type Original = { one: number; two: string }
type WrappedOriginal = Wrapped<Original>
// WrappedOriginal.one has type { value: number }When keys are known, mapped types provide precise typings that index signatures alone cannot. Use them together with utility types such as Extract or Exclude to pick key subsets; see Deep Dive: Using Extract<T, U> to Extract Types from Unions and Using Exclude<T, U>: Excluding Types from a Union.
Validating runtime shapes and avoiding type assertions
Index signatures often encourage runtime checks or type assertions. Prefer runtime validation over blind asserts. For example, avoid:
// risky
const obj: any = JSON.parse(someJson)
const settings = obj as { [k: string]: string }Instead, validate keys and types at runtime and narrow them before asserting. When you must use assertions, understand the trade-offs discussed in Type Assertions (as keyword or <>) and Their Risks and prefer safer constructs like type guards.
Advanced Techniques
Once you have basic index signatures down, combine them with conditional types, mapped types, and utility types to achieve powerful patterns. For example, use Extract and Exclude to filter union keys used in Record to build partial dictionaries for specific subsystems. Use NonNullable to prevent null or undefined creeping into dictionary values when reading from external sources; see Using NonNullable
Another advanced pattern is to create hybrid interfaces that expose known properties and an index signature for extensions, then use generics to constrain allowable extension keys. You can also create deep index signatures with recursive types for JSON-like structures, but take care to avoid overly broad types that defeat type-safety. To ensure runtime correctness, pair these types with validation utilities or schema validators.
Performance-wise, large mapped types or deep conditional types can slow down the TypeScript compiler in complex projects. Keep types focused and test compile performance as you scale. For patterns that need stricter shaping, prefer Record over broad index signatures and use explicit unions for known key sets.
Best Practices & Common Pitfalls
Dos:
- Use index signatures when value shape is uniform and keys are unknown.
- Prefer Record<K, V> if the key set is known or can be expressed as a union.
- Combine index signatures with Readonly when immutability is desired.
- Use runtime checks and narrowing before treating a dynamic value as a specific type.
- Use utility types like Pick, Omit, Partial to refine types derived from index-based structures. See guides on Pick and Omit.
Don'ts:
- Don't use overly broad index signatures that include any unless necessary.
- Avoid mixing incompatible explicit properties and narrow index signatures.
- Avoid rampant use of type assertions to silence the compiler; refer to Non-null Assertion Operator (!) Explained for alternatives.
Common pitfalls:
- Forgetting number vs string index differences. Numbers are coerced to strings at runtime but typed differently by TypeScript.
- Creating recursive index signatures without base cases, which can confuse the compiler and tooling.
- Using index signatures to represent objects with heterogeneous values; consider unions or discriminated unions instead and learn narrowing techniques from Type Narrowing with instanceof Checks in TypeScript.
Real-World Applications
-
API response maps: When an API returns an object keyed by id, model it with index signatures to preserve autocompletion for values and ensure consistent shapes. Pair with runtime validation to protect against unexpected shapes.
-
Feature flags and config maps: Use Record or index signatures for configuration keyed by feature names, and combine with Partial when features are optional. For Partial usage, see Using Partial
: Making All Properties Optional . -
Caches and memoization: Typed caches keyed by arguments or ids benefit from generic dictionary types to keep cache operations type-safe and predictable.
-
Style maps and theme tokens: A style system that stores CSS values keyed by token names can use index signatures with value unions to enforce allowed token types.
Conclusion & Next Steps
Index signatures are an essential TypeScript tool for typing objects with dynamic keys. They provide a practical balance between flexibility and type safety when modeling dictionary-like structures. Start by using string or number index signatures for simple use cases, then adopt Record, mapped types, and generics for more precise control. Continue exploring utility types like Pick and Omit, and study generic patterns to make your index-based types reusable.
Next steps: read the guides linked in this article on generics, utility types, and narrowing to deepen your understanding and apply these patterns to real codebases.
Enhanced FAQ
Q: What is the difference between an index signature and using Record? A: Record<K, V> is a type helper that maps a known key union K to a value type V. It is precise when you know the key set. An index signature like { [key: string]: V } allows any string key and is better for truly dynamic keys. Use Record when keys are explicit or enumerable, and index signatures when keys are arbitrary.
Q: Can I mix explicit properties with an index signature? A: Yes, you can mix them, but the index signature must be compatible with explicit properties. For example, if you have an explicit property version: number and an index signature [k: string]: string, TypeScript will error because version's type is incompatible. Make the index signature [k: string]: string | number to accommodate explicit props.
Q: How do number index signatures behave differently? A: Number index signatures apply when you access properties using numeric indices, e.g., obj[0]. At runtime numeric keys are converted to strings, but TypeScript enforces that the number index value type must be a subtype of the string index value type when both exist. Use number index signatures for array-like objects.
Q: Are index signatures compatible with readonly properties? A: Yes, you can declare a readonly index signature to prevent assignment to any dynamic key. For example: { readonly [k: string]: number } marks all properties as non-writable from the type system perspective.
Q: How do I avoid using any with index signatures? A: Avoid declaring { [k: string]: any } unless you absolutely need it. Specify precise value types or unions. If values are heterogeneous, model them with unions and discriminated unions so TypeScript can narrow correctly. When runtime unknowns exist, validate and narrow before use.
Q: How can I combine index signatures with mapped types?
A: Mapped types transform a set of keys into properties with modified value types. They are ideal when key sets are known. For example, type Wrapped
Q: Should I validate dynamic objects at runtime? A: Yes. TypeScript checks types at compile time but does not enforce runtime behavior. Use runtime guards, parsers, or schema validators to ensure objects match your types, particularly for external input like JSON from APIs. When writing guards, apply techniques from Type Narrowing with typeof Checks in TypeScript and Type Narrowing with the in Operator in TypeScript.
Q: What about deep nested structures with dynamic keys? A: For deeply nested dynamic shapes, prefer explicit recursive types with clear base cases, or use JSON-type utilities. Consider using validation libraries to enforce runtime safety. Recursive index signatures can become hard to manage and hurt editor performance, so balance convenience with clarity.
Q: How do I choose between index signatures and discriminated unions? A: If keys are dynamic but values are homogeneous, index signatures are a good choice. If the values are heterogeneous and depend on a discriminator, use discriminated unions so TypeScript can narrow the value type depending on a property. Use narrowing strategies such as instanceof or custom type guards when needed; see Type Narrowing with instanceof Checks in TypeScript.
Q: Any tips to optimize type-checking performance with index-heavy types? A: Large mapped or conditional types can slow down the compiler. Keep types modular and avoid deeply nested conditional types. Prefer Record and simpler generics for common dictionary patterns. Measure compile times when introducing complex types and refactor into smaller, reusable types when necessary.
If you want more guided examples on utility types that often pair with index signatures, check our Introduction to Utility Types: Transforming Existing Types. For specific cases like excluding nulls from dictionary values, read Using NonNullable
Happy typing — and remember: just because a key is dynamic does not mean your types have to be weak.
