Question
I am trying to define an object type where every value from an enum is used as a key:
enum Option {
ONE = 'one',
TWO = 'two',
THREE = 'three'
}
interface OptionRequirement {
someBool: boolean;
someString: string;
}
interface OptionRequirements {
[key: Option]: OptionRequirement;
}
This feels like it should work, but TypeScript gives this error:
An index signature parameter type cannot be a union type. Consider using a mapped object type instead.
Why does this happen, and what is the correct way to type an object whose keys must be all values of the Option enum?
Short Answer
By the end of this page, you will understand why TypeScript index signatures only work with broad key types like string, number, or symbol, and why enum-based key sets must use mapped types instead. You will also learn how to use Record, how mapped types enforce exact keys, and how to avoid common mistakes when typing objects with enum or union keys.
Concept
In TypeScript, an index signature describes objects whose keys are not known exactly ahead of time, but follow a broad category such as:
[key: string]: ValueType
That means any string key is allowed.
Your enum:
enum Option {
ONE = 'one',
TWO = 'two',
THREE = 'three'
}
represents a specific set of allowed keys, not all strings. TypeScript treats this as a finite union of literal values like:
'one' | 'two' | 'three'
An index signature cannot use that kind of finite union. Index signatures are only for general key categories such as:
stringnumbersymbol- template literal pattern types in some cases
If you want to say:
- these exact keys are allowed
- each key maps to the same value shape
then you should use a .
Mental Model
Think of an index signature as a rule for a giant mailbox wall:
[key: string]: ...means any mailbox label made of text is allowed
Think of a mapped type as a checklist:
- only these three mailboxes must exist:
one,two,three
So:
- index signature = open-ended dictionary
- mapped type = exact set of known keys
Your enum is a checklist, not an open-ended dictionary. That is why TypeScript asks you to use a mapped type.
Syntax and Examples
Correct syntax with a mapped type
enum Option {
ONE = 'one',
TWO = 'two',
THREE = 'three'
}
interface OptionRequirement {
someBool: boolean;
someString: string;
}
type OptionRequirements = {
[K in Option]: OptionRequirement;
};
This creates a type equivalent to:
type OptionRequirements = {
one: OptionRequirement;
two: OptionRequirement;
three: OptionRequirement;
};
Example object
const requirements: OptionRequirements = {
[Option.ONE]: { someBool: true, : },
[.]: { : , : },
[.]: { : , : }
};
Step by Step Execution
Consider this code:
enum Option {
ONE = 'one',
TWO = 'two'
}
interface OptionRequirement {
someBool: boolean;
someString: string;
}
type OptionRequirements = Record<Option, OptionRequirement>;
const config: OptionRequirements = {
[Option.ONE]: { someBool: true, someString: 'A' },
[Option.TWO]: { someBool: false, someString: 'B' }
};
Step 1: TypeScript reads the enum
Option.ONE // 'one'
Option.TWO // 'two'
The enum values form a small allowed key set.
Real World Use Cases
This pattern is common whenever you have a fixed list of allowed keys.
Feature flags
enum Feature {
DARK_MODE = 'darkMode',
BETA_SEARCH = 'betaSearch'
}
type FeatureFlags = Record<Feature, boolean>;
Validation rules by field
type Field = 'email' | 'password';
type ValidationRules = Record<Field, string[]>;
API status handlers
type Status = 'success' | 'error' | 'loading';
type Handlers = Record<Status, () => void>;
Environment-specific config
Real Codebase Usage
In real codebases, developers usually use this concept in a few common ways.
1. Record for lookup tables
const labels: Record<Option, string> = {
[Option.ONE]: 'First option',
[Option.TWO]: 'Second option',
[Option.THREE]: 'Third option'
};
This is common for labels, routes, constants, and UI text.
2. Configuration objects with exact coverage
When every enum value must be handled, Record helps prevent missing cases.
const optionConfig: Record<Option, OptionRequirement> = {
[Option.ONE]: { someBool: true, someString: 'A' },
[Option.TWO]: { someBool: false, someString: },
[.]: { : , : }
};
Common Mistakes
1. Using an index signature for a limited set of keys
Broken code:
interface OptionRequirements {
[key: Option]: OptionRequirement;
}
Why it fails:
Optionis a finite set of keys- index signatures expect a broad key category like
string
Fix:
type OptionRequirements = Record<Option, OptionRequirement>;
2. Using string when you want exact keys
Broken idea:
interface OptionRequirements {
[key: string]: OptionRequirement;
}
Problem:
- this allows any string key
- you lose protection against invalid keys
Example:
Comparisons
| Concept | Best for | Allows exact key set? | Example |
|---|---|---|---|
| Index signature | Open-ended dictionaries | No | { [key: string]: number } |
| Mapped type | Known finite keys | Yes | { [K in Option]: Value } |
Record<K, V> | Shorthand for mapped types | Yes | Record<Option, Value> |
Partial<Record<K, V>> | Optional known keys | Partially | Partial<Record<Option, Value>> |
Index signature vs mapped type
Cheat Sheet
// Enum keys
enum Option {
ONE = 'one',
TWO = 'two',
THREE = 'three'
}
interface OptionRequirement {
someBool: boolean;
someString: string;
}
Use this for exact enum-based keys
type OptionRequirements = Record<Option, OptionRequirement>;
or
type OptionRequirements = {
[K in Option]: OptionRequirement;
};
Do not use this
interface OptionRequirements {
[key: Option]: OptionRequirement;
}
Use Partial if keys are optional
FAQ
Why can't I use an enum in an index signature?
Because index signatures are for broad key types like string or number, not for a limited union of known values.
What should I use instead of [key: Option]?
Use a mapped type or Record<Option, ValueType>.
Is Record<Option, T> the same as [K in Option]: T?
Yes, for this use case they are effectively the same.
When should I use an index signature?
Use it when keys are not known ahead of time and any string or number key may appear.
How do I make enum-based keys optional?
Use:
Partial<Record<Option, T>>
Will TypeScript force me to include every enum value?
Yes, if you use Record<Option, T> without Partial.
Can I use string unions instead of enums?
Yes. This also works well:
= | | ;
= <, >;
Mini Project
Description
Build a small TypeScript configuration map for notification channels. This demonstrates how to use a fixed set of enum values as object keys, and how TypeScript ensures every channel has a configuration entry.
Goal
Create a type-safe notification settings object where every notification channel defined in an enum has a configuration record.
Requirements
Requirement 1 Requirement 2 Requirement 3
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.
Angular Dependency Injection: Fix "Can't Resolve All Parameters for Component" Errors
Learn why Angular shows "Can't resolve all parameters for component" and how to fix service injection issues in components.