This TypeScript error occurs when you define both specific properties and an index signature on the same type, making the specific properties unreachable. The index signature acts as a catch-all that overshadows any explicitly defined properties, preventing TypeScript from accessing them safely.
The "Property is unreachable because index signature covers all properties" error (TS2411) happens when you combine explicit property definitions with an index signature in the same type. An index signature like `[key: string]: T` means "this object can have any string key with value type T." When you also define specific properties like `name: string`, those properties become unreachable because the index signature already covers all possible keys, including 'name'. This creates a type safety issue: TypeScript cannot guarantee that accessing `obj.name` will return a string, because the index signature allows any string key to have any value of type T. Even if T is string, the index signature doesn't enforce that 'name' specifically returns a string - it could theoretically be overridden. The error prevents you from creating types that are both open (allow arbitrary properties) and closed (have specific typed properties) in the same declaration. You need to choose one approach or restructure your types to separate concerns.
Instead of mixing index signatures with explicit properties, create separate types and combine them:
// WRONG - causes TS2411 error
interface User {
name: string;
age: number;
[key: string]: any; // Index signature makes 'name' and 'age' unreachable
}
// CORRECT - separate types
interface UserBase {
name: string;
age: number;
}
type User = UserBase & {
[key: string]: any;
};
// Usage
const user: User = {
name: "Alice",
age: 30,
extraField: "value", // Allowed by index signature
};
console.log(user.name); // Type-safe: string
console.log(user.age); // Type-safe: numberThe intersection type (&) combines the specific properties with the index signature while maintaining type safety.
If you need a type with required properties plus arbitrary additional ones, use Record with intersection:
// Using Record type
type User = {
name: string;
age: number;
} & Record<string, any>;
// Alternative with interface extension
interface UserBase {
name: string;
age: number;
}
interface User extends UserBase {
[key: string]: any;
}
// Note: This still causes TS2411! Use type intersection instead.
// Better: Use type alias with intersection
type User = UserBase & Record<string, any>;
// For specific value types
type Config = {
apiUrl: string;
timeout: number;
} & Record<string, string | number | boolean>;
const config: Config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: true, // Additional boolean property
retries: 3, // Additional number property
};The Record<string, any> creates an index signature that allows any additional string properties.
If you know all possible property names, use optional properties instead of an index signature:
// Instead of index signature for optional metadata
interface Product {
id: string;
name: string;
price: number;
metadata?: {
[key: string]: any; // Index signature in nested object is fine
};
}
// Or use a specific type for additional data
interface Product {
id: string;
name: string;
price: number;
tags: string[];
attributes: Record<string, string | number>;
}
// For React component props
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
className?: string;
disabled?: boolean;
// Instead of [key: string]: any, use specific optional props
'data-testid'?: string;
'aria-label'?: string;
}This approach is more type-safe and makes your API clearer.
When working with objects that might have arbitrary properties, use type guards:
type User = {
name: string;
age: number;
} & Record<string, unknown>;
function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.name === 'string' &&
typeof obj.age === 'number'
);
}
// Safe access with type guards
function getUserName(user: User): string {
return user.name; // Type-safe because we know User has name
}
// Accessing arbitrary properties
function getOptionalField(user: User, field: string): unknown {
return user[field]; // Returns unknown, need to check type
}
// Type-safe access with narrowing
if (typeof user.customField === 'string') {
console.log(user.customField.toUpperCase());
}Type guards provide runtime safety while maintaining TypeScript's type checking.
If you need truly dynamic key-value pairs, consider using Map instead of objects with index signatures:
// Using Map for dynamic properties
interface User {
name: string;
age: number;
metadata: Map<string, any>; // Instead of [key: string]: any
}
const user: User = {
name: "Bob",
age: 25,
metadata: new Map([
['role', 'admin'],
['lastLogin', new Date()],
['preferences', { theme: 'dark' }]
])
};
// Type-safe access to known properties
console.log(user.name);
// Access dynamic properties via Map
const role = user.metadata.get('role');
if (typeof role === 'string') {
console.log(role.toUpperCase());
}
// Alternative: Use a separate object for dynamic properties
interface User {
name: string;
age: number;
extra: Record<string, any>;
}
const user2: User = {
name: "Charlie",
age: 35,
extra: {
department: "Engineering",
salary: 80000
}
};Maps provide better type safety and performance for dynamic key-value stores.
For complex scenarios, use discriminated unions or custom type predicates:
// Discriminated union for different object shapes
type ApiResponse =
| { type: 'user'; data: UserData; metadata: Record<string, any> }
| { type: 'product'; data: ProductData; tags: string[] }
| { type: 'error'; message: string; code: number };
function handleResponse(response: ApiResponse) {
switch (response.type) {
case 'user':
// response.data is UserData
// response.metadata is Record<string, any>
break;
case 'product':
// response.data is ProductData
// response.tags is string[]
break;
case 'error':
// response.message is string
// response.code is number
break;
}
}
// Custom type predicate for validation
function hasProperty<T extends object, K extends string>(
obj: T,
key: K
): obj is T & Record<K, any> {
return key in obj;
}
const obj = { name: 'Alice', age: 30 } as Record<string, any>;
if (hasProperty(obj, 'email')) {
// obj.email is now typed as any
console.log(obj.email);
}These patterns provide better type safety than index signatures.
### Understanding Index Signatures
Index signatures in TypeScript define the type for properties when the property name isn't known at compile time. They come in two forms:
// String index signature
interface StringIndex {
[key: string]: number;
}
// Number index signature
interface NumberIndex {
[key: number]: string;
}Key constraints:
1. All explicit property types must be assignable to the index signature type
2. You can only have one string index signature and one number index signature per type
3. If both string and number index signatures exist, number index return type must be subtype of string index return type
### Why Properties Become Unreachable
When TypeScript sees [key: string]: T, it means "for any string key, the value is T." If you also define name: string, accessing obj.name should return string. But the index signature says it returns T. If T is any or unknown, there's no guarantee obj.name is string.
TypeScript's type system is structural, not nominal. The property 'name' isn't special - it's just another string key. The index signature already covers it, making the explicit declaration redundant and potentially contradictory.
### Workarounds and Alternatives
1. Intersection Types (Recommended):
type MyType = {
knownProp: string;
anotherProp: number;
} & Record<string, any>;2. Type Assertions (Use Sparingly):
interface UnsafeType {
name: string;
[key: string]: any;
} as any; // Not recommended3. Separate Interfaces:
interface KnownProps {
name: string;
age: number;
}
interface DynamicProps {
[key: string]: any;
}
type Combined = KnownProps & DynamicProps;4. Generic Constraints:
function process<T extends Record<string, any>>(obj: T & { id: string }) {
// obj has id: string plus any other properties
}### Real-World Use Cases
API Responses:
type ApiResponse<T> = {
data: T;
status: number;
} & Record<string, any>; // For headers, metadata, etc.Configuration Objects:
type AppConfig = {
apiUrl: string;
timeout: number;
} & Record<string, string | number | boolean>;Component Props (React/Vue):
// Instead of [key: string]: any for HTML attributes
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
};### TypeScript Configuration
If you're working with external libraries that use index signatures, ensure your tsconfig.json has appropriate settings:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
// These help catch index signature issues early
"noUncheckedIndexedAccess": true, // Adds undefined to index accesses
"exactOptionalPropertyTypes": true // Makes optional properties more strict
}
}The noUncheckedIndexedAccess flag is particularly useful as it makes obj[key] return T | undefined instead of just T, reminding you that properties accessed via index might not exist.
### Migration Strategy
If you have existing code with mixed index signatures and explicit properties:
1. Identify all occurrences: Search for \[key: string\]: or \[key: number\]:
2. Separate concerns: Move explicit properties to base interfaces
3. Use intersection types: Combine with Record<string, any> or similar
4. Add type guards: For runtime safety with dynamic properties
5. Test thoroughly: Index signature changes can affect type inference broadly
Remember: Index signatures are powerful but should be used judiciously. They sacrifice type safety for flexibility. Always prefer explicit property definitions when possible.
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