This TypeScript error occurs when you try to use a generic type parameter as an index signature key, which is not allowed. Index signatures only support specific key types like string, number, symbol, or template literal types. The fix involves using mapped types, keyof constraints, or redesigning your type structure.
The "Index signature parameter type cannot be generic" error appears when you attempt to use a generic type parameter as the key type in an index signature. Index signatures in TypeScript define the shape of objects when accessing properties with dynamic keys, and they have strict requirements for what types can be used as keys. TypeScript only allows specific types for index signature keys: - Primitive types: `string`, `number`, `symbol` - Template literal types (e.g., ``x-${string}``) - Union types consisting only of these allowed types Generic type parameters like `<T>` cannot be used directly because TypeScript needs to know the concrete key type at compile time for type safety and performance reasons. This restriction ensures that TypeScript can properly check property access and assignment operations. The error typically occurs when you're trying to create flexible object types with dynamic keys based on generic parameters, but you need to use alternative TypeScript features like mapped types or `keyof` constraints instead.
Instead of trying to use generic type parameters in index signatures, use mapped types which are designed for this purpose:
// ❌ WRONG - Causes error
interface GenericIndex<T> {
[key: T]: string; // Error: Index signature parameter type cannot be generic
}
// ✅ CORRECT - Use mapped type
type GenericMapped<T extends string | number | symbol> = {
[K in T]: string;
};
// Example usage
type StringKeys = GenericMapped<"a" | "b" | "c">;
// Result: { a: string; b: string; c: string; }
// ✅ Also works with keyof constraints
type PickProperties<T, K extends keyof T> = {
[P in K]: T[P];
};
// Example: Pick specific properties from an object type
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserNameAndEmail = PickProperties<User, "name" | "email">;
// Result: { name: string; email: string; }Mapped types iterate over union types and create object types with those keys.
For dictionary objects where keys come from a generic type, use TypeScript's built-in Record utility type:
// ❌ WRONG
interface Dictionary<T extends string> {
[key: T]: any; // Error
}
// ✅ CORRECT - Use Record
type Dictionary<T extends string | number | symbol> = Record<T, any>;
// Example usage
type StatusCodes = Dictionary<200 | 404 | 500>;
// Equivalent to: { 200: any; 404: any; 500: any; }
type StringDictionary = Dictionary<string>;
// Equivalent to: { [key: string]: any; }
// ✅ You can also create your own Record-like type
type MyRecord<K extends keyof any, T> = {
[P in K]: T;
};
// Usage with constraints
type Config<T extends string> = MyRecord<T, boolean>;
const config: Config<"darkMode" | "notifications"> = {
darkMode: true,
notifications: false,
};The Record<K, T> type constructs an object type with keys of type K and values of type T.
If you need an index signature, constrain the generic parameter to one of the allowed types:
// ❌ WRONG - Generic without constraint
interface FlexibleObject<T> {
[key: T]: string; // Error
}
// ✅ CORRECT - Constrain to allowed types
interface FlexibleObject<T extends string | number | symbol> {
[key: T]: string; // Now works because T is constrained
}
// ✅ More specific constraints
interface StringKeyObject<T extends string> {
[key: T]: string;
}
interface NumberKeyObject<T extends number> {
[key: T]: string;
}
// ✅ Using template literal types
interface PrefixedKeys<T extends string> {
[key: `prefix-${T}`]: string;
}
// Example usage
type MyPrefixed = PrefixedKeys<"a" | "b">;
// Result: { "prefix-a": string; "prefix-b": string; }
// ✅ Union type constraints
interface MixedKeys<T extends string | number> {
[key: T]: string;
}By constraining T to string | number | symbol, TypeScript knows it's a valid index signature type.
For advanced use cases, combine conditional types with inference:
// ❌ WRONG - Complex generic in index signature
interface ComplexIndex<T> {
[key: T extends string ? `key-${T}` : never]: string; // Error
}
// ✅ CORRECT - Use conditional mapped type
type ComplexMapped<T> = T extends string ? {
[K in `key-${T}`]: string;
} : never;
// Example usage
type Result = ComplexMapped<"a" | "b">;
// Result: { "key-a": string; "key-b": string; }
// ✅ Another example with inference
type ExtractKeys<T> = T extends { [key: infer K]: any } ? K : never;
// ✅ Create type-safe dictionaries with validation
type ValidatedDictionary<Keys extends string, Value> = {
[K in Keys]: Value;
} & {
// Additional methods or properties
getKeyCount(): number;
};
// Implementation
function createDictionary<Keys extends string, Value>(
keys: Keys[],
defaultValue: Value
): ValidatedDictionary<Keys, Value> {
const dict = {} as any;
for (const key of keys) {
dict[key] = defaultValue;
}
dict.getKeyCount = () => keys.length;
return dict;
}
const myDict = createDictionary(["a", "b", "c"], 0);
// Type: ValidatedDictionary<"a" | "b" | "c", number>Conditional types allow you to create complex type transformations while staying within TypeScript's type system constraints.
Sometimes the best solution is to rethink your type design:
// ❌ Problematic design requiring generic index signatures
interface Problematic<T> {
data: { [key: T]: any }; // Would cause error if T is generic
}
// ✅ Alternative 1: Use tuple or array for ordered data
interface Alternative1<T> {
data: T[]; // Array of values
keys: string[]; // Separate array for keys
}
// ✅ Alternative 2: Use Map or Set for dynamic collections
interface Alternative2<T> {
data: Map<string, T>; // Map preserves key-value pairs
}
// ✅ Alternative 3: Union type of possible states
type Alternative3 =
| { type: "stringKeys"; data: Record<string, any> }
| { type: "numberKeys"; data: Record<number, any> }
| { type: "specificKeys"; data: { a: any; b: any; c: any } };
// ✅ Alternative 4: Factory function pattern
function createDataStore<T extends Record<string, any>>(initialData: T) {
return {
data: initialData,
get<K extends keyof T>(key: K): T[K] {
return this.data[key];
},
set<K extends keyof T>(key: K, value: T[K]) {
this.data[key] = value;
}
};
}
// Usage
const store = createDataStore({
name: "John",
age: 30,
active: true
});
store.get("name"); // string
store.set("age", 31); // OKConsider whether you truly need dynamic keys or if a more structured approach would be better.
As a last resort, you can use type assertions, but use them carefully:
// ⚠️ Use with caution - type assertion bypasses type checking
interface WithAssertion<T> {
data: { [key: string]: any } as { [key in T & (string | number | symbol)]: any };
}
// Better approach: type guard function
function isRecordWithKeys<T extends string>(
obj: any,
keys: T[]
): obj is Record<T, any> {
if (typeof obj !== "object" || obj === null) return false;
return keys.every(key => key in obj);
}
// Usage with type guard
function processData<T extends string>(data: any, expectedKeys: T[]) {
if (isRecordWithKeys(data, expectedKeys)) {
// TypeScript now knows data is Record<T, any>
console.log(data);
} else {
throw new Error("Invalid data structure");
}
}
// Another approach: indexed access types
type ValueOf<T> = T[keyof T];
interface Container<T> {
values: T;
getValue<K extends keyof T>(key: K): T[K];
setValue<K extends keyof T>(key: K, value: T[K]): void;
}
// Usage
const container: Container<{ x: number; y: string }> = {
values: { x: 1, y: "hello" },
getValue(key) { return this.values[key]; },
setValue(key, value) { this.values[key] = value; }
};Type assertions should be used sparingly and only when you have external guarantees about the type structure.
### Understanding TypeScript's Index Signature Restrictions
TypeScript restricts index signature key types to string, number, symbol, and template literal types for several important reasons:
1. Type Safety: Index signatures define the shape for *all* possible property accesses with a certain key type. If generic types were allowed, TypeScript couldn't guarantee type safety for arbitrary property accesses.
2. Performance: Concrete key types allow TypeScript to optimize type checking and compilation. Generic types would require runtime type information or complex compile-time analysis.
3. JavaScript Semantics: JavaScript objects only support string and symbol keys (numbers are converted to strings). TypeScript's restrictions align with JavaScript's actual behavior.
### Mapped Types vs Index Signatures
While they look similar, mapped types and index signatures serve different purposes:
Index Signatures:
- Define shape for *all* properties with keys of a certain type
- Example: { [key: string]: number } means *any* string key returns a number
- Cannot use generic type parameters as keys
Mapped Types:
- Transform existing types by iterating over their keys
- Example: { [K in keyof T]: T[K] } preserves the exact keys of T
- Can use generic type parameters with constraints
- Create new types based on existing ones
### Common Patterns and Solutions
#### Pattern 1: Dictionary with Generic Value Type
// ✅ Use Record
type Dictionary<T> = Record<string, T>;
// ✅ Or explicit mapped type
type Dictionary2<T> = {
[key: string]: T;
};#### Pattern 2: Type-Safe Configuration Objects
type ConfigOptions = "darkMode" | "notifications" | "analytics";
type Config = Record<ConfigOptions, boolean>;
// Or with default values
type ConfigWithDefaults = {
[K in ConfigOptions]: boolean;
} & {
version: string;
};#### Pattern 3: Dynamic Key Generation
type DynamicKeys<Prefix extends string, Keys extends string> = {
[K in Keys as `${Prefix}-${K}`]: boolean;
};
type Prefixed = DynamicKeys<"app", "theme" | "language">;
// Result: { "app-theme": boolean; "app-language": boolean; }### Template Literal Types in Index Signatures
TypeScript 4.1+ allows template literal types in index signatures, but with limitations:
// ✅ Valid - template literal type
interface Headers {
[header: `x-${string}`]: string;
}
// ❌ Invalid - generic in template literal
interface Invalid<T extends string> {
[key: `prefix-${T}`]: string; // Still not allowed
}
// ✅ Workaround - mapped type with template literals
type Valid<T extends string> = {
[K in T as `prefix-${K}`]: string;
};### When to Consider Alternative Approaches
If you frequently encounter this error, consider:
1. Using Maps instead of objects for truly dynamic key-value stores
2. Designing with union types instead of trying to make everything generic
3. Using factory functions that return properly typed objects
4. Leveraging existing utility types like Record, Partial, Required, etc.
### Debugging Tips
1. Use tsc --noEmit --strict to catch these errors early
2. Check if your generic parameter needs extends string | number | symbol constraint
3. Consider whether you're trying to use an index signature when a mapped type would be more appropriate
4. Use TypeScript playground to experiment with different approaches
Remember: TypeScript's type system is designed to catch potential runtime errors at compile time. While generic index signatures might seem convenient, their restriction prevents entire categories of type-unsafe code.
Function 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