517

I'm trying to use the following pattern:

enum Option {
  ONE = 'one',
  TWO = 'two',
  THREE = 'three'
}

interface OptionRequirement {
  someBool: boolean;
  someString: string;
}

interface OptionRequirements {
  [key: Option]: OptionRequirement;
}

This seems very straightforward to me, however I get the following error:

An index signature parameter type cannot be a union type. Consider using a mapped object type instead.

What am I doing wrong?

vdshb
  • 1,949
  • 2
  • 28
  • 40
john maccarthy
  • 5,463
  • 3
  • 11
  • 14

8 Answers8

745

You can use TS "in" operator and do this:

enum Options {
  ONE = 'one',
  TWO = 'two',
  THREE = 'three',
}
interface OptionRequirement {
  someBool: boolean;
  someString: string;
}
type OptionRequirements = {
  [key in Options]: OptionRequirement; // Note the "in" operator.
}

More about the in operator

Victor
  • 3,841
  • 2
  • 37
  • 63
Nacho Justicia Ramos
  • 8,313
  • 1
  • 16
  • 26
  • 26
    Erm, this doesn't compile? The [TypeScript playground says](https://www.typescriptlang.org/play/index.html#code/KYOwrgtgBA8gDgFwJYHsQGcoG8BQVYByAolALxQDkawFANHlACoDqMZlCA7inQ4wBIAlIiXIUEACwBOwGvQC+OJCATApAMwCGAY2CxEqEIOABHMEhkRQCbA3QorAIRQoANgC4oAIxevgmkABuOwdgAGUEKWUAc090SJjgxWVVDR09eGQ0YzMLYCsVTFx8AG0Aa2AATyhlfSyMAF1PTMMc80trQKgAem6oAhRVKElNGwAiCurlMYA6HHkgA): "A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type." – meriton Aug 22 '19 at 17:03
  • 39
    Change `interface OptionRequirements` to `type OptionRequirements` – Tyler Rick Aug 23 '19 at 21:07
  • 8
    this actually doesn't work for me: A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type – Tyguy7 Nov 15 '19 at 21:41
  • Could you please tell us what version of Typescript are you using? – Nacho Justicia Ramos Nov 16 '19 at 09:19
  • "Change interface OptionRequirements to type OptionRequirements": it depends on the Typescript version you are using – Nacho Justicia Ramos Nov 21 '19 at 10:09
  • 4
    I have edited this answer to use a mapped type alias instead of an interface. The original answer does not compile under any version of TypeScript that I've seen, and certainly does not compile under the current version of TypeScript (4.0 as of Aug 2020). @NachoJusticiaRamos, if you could demonstrate that your original version actually works somewhere, in some version of TypeScript, then I'd be happy to revert the edit, along with a description of the environment you need to use to have it work. Cheers! – jcalz Aug 24 '20 at 03:51
  • 6
    Can someone explain a little bit what's going on here? What is the underlying problem TS encounters in setting an enum variable as a key? And why does changing "interface" to "type" solve the issue? – user199710 Jun 22 '21 at 12:23
  • 1
    Using a type solves the issue but if you want/need an interface for any reason you can extend the type: `interface IOptionRequirements extends OptionRequirements { // empty, or additional properties here };` – Ed Brissenden Sep 23 '21 at 10:25
  • @jcalz It's kinda odd that this non-compiling answer gets fixed, when there is [another answer that got it correct (i.e. uses a mapped type alias) 19 months earlier](https://stackoverflow.com/a/54438350/8910547). Even more odd given that the original author already reverted the same fix attempt before. – Inigo Dec 21 '21 at 09:04
229

The simplest solution is to use Record

type OptionRequirements = Record<Options, OptionRequirement>

You can also implement it yourself as:

type OptionRequirements = {
  [key in Options]: OptionRequirement;
}

This construct is only available to type, but not interface.

The problem in your definition is saying the key of your interface should be of type Options, where Options is an enum, not a string, number, or symbol.

The key in Options means "for those specific keys that's in the union type Options".

type alias is more flexible and powerful than interface.

If your type does not need to be used in class, choose type over interface.

unional
  • 14,651
  • 5
  • 32
  • 56
  • that was cool. Now what if I need this variable to be undefined, how can you initialize it later? – Kat Lim Ruiz Apr 28 '21 at 03:41
  • @KatLimRuiz: `[key in Options]: OptionRequirement | undefined` – unional Apr 28 '21 at 04:52
  • Interesting... if you do something like `keyof Options` within the `Record` typing, it works fine, but if you do it via the "implement it yourself" route you get a syntax error, unable to do `[keyof Options]: OptionRequirement`. In my case, my `Option` is a type, not an enum. – fullStackChris Jan 06 '22 at 11:51
  • 3
    Also, I wonder if there is a way to just enforce that the key be _in_ the enum, but not have typescript complain that you have to exhaustively use all members of that enum... – fullStackChris Jan 25 '22 at 10:48
  • Ah, of course... using `Exclude` you can constrain which members should be implemented in your `Record`! – fullStackChris Jan 25 '22 at 10:49
  • @fullStackChris, can you give an example? – AntonOfTheWoods Feb 14 '22 at 01:26
  • @AntonOfTheWoods - I just made an example on TypeScript playground, but big brain stack overflow won't let me post the link since it's too long.... they wont let me use something like bitly either... any ideas on how to share it? – fullStackChris Feb 14 '22 at 10:55
113

In my case:

export type PossibleKeysType =
  | 'userAgreement'
  | 'privacy'
  | 'people';

interface ProviderProps {
  children: React.ReactNode;
  items: {
    //   ↙ this colon was issue
    [key: PossibleKeysType]: Array<SectionItemsType>;
  };
}

I fixed it by using in operator instead of using :

~~~

interface ProviderProps {
  children: React.ReactNode;
  items: {
    //     ↙ use "in" operator
    [key in PossibleKeysType]: Array<SectionItemsType>;
  };
}
AmerllicA
  • 29,059
  • 15
  • 130
  • 154
45

I had some similar problem but my case was with another field property in interface so my solution as an example with optional field property with an enum for keys:

export enum ACTION_INSTANCE_KEY {
  cat = 'cat',
  dog = 'dog',
  cow = 'cow',
  book = 'book'
}

type ActionInstances = {
  [key in ACTION_INSTANCE_KEY]?: number; // cat id/dog id/cow id/ etc // <== optional
};

export interface EventAnalyticsAction extends ActionInstances { // <== need to be extended
  marker: EVENT_ANALYTIC_ACTION_TYPE; // <== if you wanna add another field to interface
}
Igor Kurkov
  • 4,318
  • 2
  • 30
  • 31
26

In my case I needed the properties to be optional, so I created this generic type.

type PartialRecord<K extends string | number | symbol, T> = { [P in K]?: T; };

Then use it as such:

type MyTypes = 'TYPE_A' | 'TYPE_B' | 'TYPE_C';

interface IContent {
    name: string;
    age: number;
}

interface IExample {
    type: string;
    partials: PartialRecord<MyTypes, IContent>;
}

Example

const example : IExample = {
    type: 'some-type',
    partials: {
        TYPE_A : {
            name: 'name',
            age: 30
        },
        TYPE_C : {
            name: 'another name',
            age: 50
        }
    }
}
Alazzawi
  • 261
  • 3
  • 3
21

Instead of using an interface, use a mapped object type

enum Option {
  ONE = 'one',
  TWO = 'two',
  THREE = 'three'
}

type OptionKeys = keyof typeof Option;

interface OptionRequirement {
  someBool: boolean;
  someString: string;
}

type OptionRequirements = {                 // note type, not interface
  [key in OptionKeys]: OptionRequirement;   // key in
}
Stefan
  • 747
  • 8
  • 11
  • you don't need to add so many types, the solution of @Nacho Justicia Ramos works the ONLY thing that some people overlook is that the last type is a TYPE not an INTERFACE. Which you could create an interface from that type. – titusfx Aug 26 '21 at 08:51
9

edited

TL;DR: use Record<type1,type2> or mapped object such as:

type YourMapper = {
    [key in YourEnum]: SomeType
}

I faced a similar issue, the problem is that the allowed types for keys are string, number, symbol or template literal type.

So as Typescript suggests, we can use the mapped object type:

type Mapper = {
    [key: string]: string;
}

Notice how in a map object we are only allowed to use strings, number or symbol as keys, so if we want to use a specific string (i.e. emum or union types), we shouold use the in keyword inside the index signature. This is used to refer to the specific properties in the enum or union.

type EnumMapper = {
  [key in SomeEnum]: AnotherType;
};

On a real life example, let say we want to get this result, an object that both its keys, and its values are of specified types:

  const notificationMapper: TNotificationMapper = {
    pending: {
      status: EStatuses.PENDING,
      title: `${ENotificationTitels.SENDING}...`,
      message: 'loading message...',
    },
    success: {
      status: EStatuses.SUCCESS,
      title: ENotificationTitels.SUCCESS,
      message: 'success message...',
    },
    error: {
      status: EStatuses.ERROR,
      title: ENotificationTitels.ERROR,
      message: 'error message...'
    },
  };

In order to achieve this with Typescript, we should create the different types, and then implement them in a Record<> or with a mapped object type:

export enum EStatuses {
  PENDING = 'pending',
  SUCCESS = 'success',
  ERROR = 'error',
}

interface INotificationStatus {
  status: string;
  title: string;
  message: string;
}

//option one, Record:
type TNotificationMapper = Record<EStatuses, INotificationStatus>

//option two, mapped object:
type TNotificationMapper = {
  [key in EStatuses]:INotificationStatus;
}

Here I'm using enums, but this approach work both for enum and union types.

*NOTE- a similar syntax using the parenthesis instead of square brackets (i.e. this (...) instead of this [...], might not show any error, but it's signify a completely different thing, a function interface, so this:

interface Foo {
(arg:string):string;
}

is actually describing a function signature such as:

const foo = (arg:string) => string;
Joe
  • 197
  • 1
  • 3
  • Unfortunately, this doesn't work. It won't give syntax errors now, but try declaring a new object with type INote. The compiler will not understand invoiceId or orderId. – Marco Slooten Sep 19 '22 at 12:13
  • @MarcoSlooten thanks for the input!, I checked what you've said after and discovered I was wrong, so I researched it again and found the right approach, so thanks again..! – Joe Oct 25 '22 at 15:19
4

I had a similar issue. I was trying to use only specific keys when creating angular form validators.

export enum FormErrorEnum {
  unknown = 'unknown',
  customError = 'customError',
}

export type FormError = keyof typeof FormErrorEnum;

And the usage:

static customFunction(param: number, param2: string): ValidatorFn {
  return (control: AbstractControl): { [key: FormErrorEnum]?: any } => {
    return { customError: {param, param2} };
  };
}

This will allow for 1 - X number of keys to be used.

Questioning
  • 1,903
  • 1
  • 29
  • 50