This TypeScript error occurs when you try to define an index signature with an invalid type. Index signatures allow objects to have dynamic keys, but TypeScript restricts them to specific types for type safety and performance. The fix involves changing the index signature type to one of the allowed types: string, number, symbol, or template literal.
The "Index signature must be a string, number, symbol, or template literal type" error appears when you try to define an index signature with a type that TypeScript doesn't support. Index signatures are a way to define objects with dynamic keys, like dictionaries or maps. In TypeScript, index signatures allow you to specify the type of values that can be accessed using a particular key type. However, TypeScript restricts the key types to: - **string**: For object keys that are strings (most common) - **number**: For array-like objects or numeric keys - **symbol**: For unique symbol keys - **template literal type**: For pattern-based string keys (TypeScript 4.1+) This restriction exists because: 1. **Type safety**: These types have predictable behavior and can be properly type-checked 2. **Performance**: TypeScript's type system can optimize around these specific types 3. **JavaScript compatibility**: These correspond to actual JavaScript property key types When you see this error, you're trying to use a type like `boolean`, `any`, `unknown`, or a union type that includes disallowed types as an index signature parameter.
The most common fix is to change your index signature to use string type:
// WRONG - using boolean
interface Config {
[key: boolean]: string; // Error!
}
// CORRECT - use string instead
interface Config {
[key: string]: string;
}
// Example usage
const config: Config = {
"enabled": "true",
"debug": "false",
"maxRetries": "3"
};If you need boolean-like behavior, use string keys with boolean values:
interface Settings {
[key: string]: boolean;
}
const settings: Settings = {
darkMode: true,
notifications: false,
autoSave: true
};If you're creating an array-like structure, use number as the index type:
// WRONG - trying to use string for numeric indices
interface NumberMap {
[key: string]: number; // Works but not type-safe for array access
}
// CORRECT - use number for array indices
interface NumberArray {
[index: number]: number;
}
const scores: NumberArray = [95, 87, 92];
const firstScore = scores[0]; // Type: number
// For dictionary with string keys and number values
interface ScoresDict {
[name: string]: number;
}
const studentScores: ScoresDict = {
"alice": 95,
"bob": 87,
"charlie": 92
};Remember: number index signatures are for array-like access patterns, not general numeric keys.
In TypeScript 4.1+, you can use template literal types for pattern-based index signatures:
// WRONG - trying to use literal union
type EventHandlers = {
[event: 'click' | 'hover' | 'focus']: () => void; // Error!
};
// CORRECT - use template literal type with pattern
type EventHandlers = {
[event: `on${string}`]: () => void;
};
const handlers: EventHandlers = {
onClick: () => console.log("Clicked"),
onHover: () => console.log("Hovered"),
onFocus: () => console.log("Focused")
};
// More complex patterns
type CSSProperties = {
[property: `--${string}`]: string;
};
const cssVars: CSSProperties = {
"--primary-color": "#007bff",
"--secondary-color": "#6c757d",
"--font-size": "16px"
};Template literal types allow you to define patterns for string keys.
If you need specific keys, use a mapped type instead of an index signature:
// WRONG - union type in index signature
type Status = 'pending' | 'success' | 'error';
type StatusMap = {
[key: Status]: string; // Error!
};
// CORRECT - use mapped type
type Status = 'pending' | 'success' | 'error';
type StatusMap = {
[K in Status]: string;
};
const messages: StatusMap = {
pending: "Processing...",
success: "Completed successfully",
error: "An error occurred"
};
// Alternative: Record utility type
type StatusMap2 = Record<Status, string>;
// If you need dynamic keys with specific value types
type DynamicStatusMap = {
[key: string]: Status; // Values are Status, keys are any string
};
const userStatus: DynamicStatusMap = {
user1: 'pending',
user2: 'success',
user3: 'error'
};If using generics with index signatures, add proper constraints:
// WRONG - unconstrained generic
interface Dictionary<T> {
[key: T]: string; // Error! T could be anything
}
// CORRECT - constrain T to allowed index types
interface Dictionary<T extends string | number | symbol> {
[key: T]: string;
}
// Usage
const stringDict: Dictionary<string> = {
"key1": "value1",
"key2": "value2"
};
const numberDict: Dictionary<number> = {
1: "one",
2: "two",
3: "three"
};
// For template literal types
type PatternDictionary<T extends string> = {
[key: `prefix-${T}`]: string;
};
const prefixed: PatternDictionary<'item' | 'user'> = {
'prefix-item': "Item value",
'prefix-user': "User value"
};For truly unique keys, use symbol type:
// WRONG - trying to use unique keys with string
interface Metadata {
[key: string]: any; // Not truly unique
}
// CORRECT - use symbol for unique metadata
const metadataKey = Symbol("metadata");
interface WithMetadata {
[metadataKey]: {
createdAt: Date;
version: number;
};
[key: string]: any;
}
const obj: WithMetadata = {
name: "Example",
value: 42,
[metadataKey]: {
createdAt: new Date(),
version: 1
}
};
// Multiple symbol keys
const debugKey = Symbol("debug");
const cacheKey = Symbol("cache");
interface EnhancedObject {
[debugKey]: boolean;
[cacheKey]: Map<string, any>;
[key: string]: unknown;
}Symbols create truly unique property keys that won't conflict with string keys.
### Understanding TypeScript's Index Signature Restrictions
TypeScript restricts index signatures to specific types for several important reasons:
1. Type System Consistency: The type system needs to be able to reason about property access. With unrestricted types, TypeScript couldn't guarantee type safety.
2. Performance Optimization: The compiler can optimize property lookups and type checking when it knows the key type is limited to these specific categories.
3. JavaScript Runtime Compatibility: In JavaScript, object keys are always converted to strings (except for Symbols). TypeScript's restrictions align with this runtime behavior.
### Historical Context
In older TypeScript versions (pre-2.9), index signatures were even more restricted. The addition of template literal types in TypeScript 4.1 expanded what's possible while maintaining type safety.
### Workarounds for Complex Scenarios
Scenario 1: You need a type-safe dictionary with specific key patterns but also allow arbitrary string keys:
interface Config {
// Known required properties
apiUrl: string;
timeout: number;
// Dynamic configuration options
[key: string]: string | number;
}
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
// Additional dynamic properties
retryCount: 3,
environment: "production"
};Scenario 2: You need different value types for different key patterns:
type ComplexConfig = {
// String keys get string values
[K in `setting-${string}`]: string;
} & {
// Number keys get number values
[K: number]: number;
} & {
// Known properties
name: string;
enabled: boolean;
};
// This requires TypeScript 4.1+ and careful type definitions### Common Pitfalls
1. Confusing index signatures with mapped types:
- Index signature: {[key: string]: T} - any string key
- Mapped type: {[K in Keys]: T} - specific set of keys
2. Forgetting that numeric keys are converted to strings in JavaScript:
const obj = { 1: "one", "1": "also one" }; // Same property!3. Trying to use index signatures with methods:
// WRONG
interface Api {
[method: string]: (...args: any[]) => any; // Loses parameter/return type safety
}
// BETTER
type Api = {
getUser(id: string): Promise<User>;
createUser(data: UserData): Promise<User>;
};### TypeScript Version Considerations
- TypeScript 2.9+: Added support for number and symbol index signatures
- TypeScript 4.1+: Added template literal types for pattern-based index signatures
- TypeScript 4.4+: Improved performance and error messages for index signatures
### When Not to Use Index Signatures
Consider alternatives when:
- You know all possible keys upfront (use an interface or type with explicit properties)
- You need different value types for different keys (use a union type or conditional types)
- You need method signatures with specific parameters (define methods explicitly)
- Performance is critical (index signatures have some runtime overhead in TypeScript's type checking)
### Debugging Tips
Use these TypeScript compiler flags to debug index signature issues:
{
"compilerOptions": {
"noImplicitAny": true, // Catch implicit any in index signatures
"strictNullChecks": true, // Ensure index signatures handle null/undefined
"exactOptionalPropertyTypes": true // Strict checking of optional properties
}
}To see what TypeScript infers for an index signature:
type Debug<T> = { [K in keyof T]: T[K] };
// Hover over Debug<YourType> in your IDE to see the inferred structureFunction expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript