john maccarthy
john maccarthy

Reputation: 6333

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

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?

Upvotes: 601

Views: 270968

Answers (10)

Christian Paul Gastardo
Christian Paul Gastardo

Reputation: 1261

Alternatively, if you want to extend this to some degree and add additional properties, you can do it like this:

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

type OptionRequirement = {
  someBool: boolean;
  someString: string;
}

interface Degree {
  name: string;
  option: {[key in Options]: OptionRequirement};
}

In my case, I used it to dynamically hide some parts of an element.

enum EditElementKeyEnum {
    LABEL = 'label', PLACEHOLDER = 'placeholder', OPTIONS = 'options'
}

interface TemplateElementBase {
    name: string;
    hide?: {[key in EditElementKeyEnum]?: boolean};
}

const element: TemplateElementBase = {
    name: 'Sample',
    hide: {
      placeholder: true
    }
}

Upvotes: 2

Hamed Mahdizadeh
Hamed Mahdizadeh

Reputation: 976

A very dynamic way for matching both types of keys and type of values:

interface AdvanceSearchParam {
        page: number;
        date: string;
        type: 'archived' | 'available',
        field: 'name' | 'type' | 'group'
}

const partialParams: Partial<AdvanceSearchParam> = {page: 1}
//{page: '1'} error
//{page: 1 } true
//{ field: 'type', page: 1 } //true
//{ field: 'other' } //error

and less strict type:

{ [key in keyof AdvanceSearchParam ]?: string | number }

//{ field: 'other' } true
//{'field1: 'type' } false

Upvotes: 0

Nacho Justicia Ramos
Nacho Justicia Ramos

Reputation: 9751

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

Upvotes: 881

Joe
Joe

Reputation: 507

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;

Upvotes: 9

AmerllicA
AmerllicA

Reputation: 32572

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>;
  };
}

Upvotes: 127

Alazzawi
Alazzawi

Reputation: 291

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
        }
    }
}

Upvotes: 29

Questioning
Questioning

Reputation: 2043

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.

Upvotes: 4

Stefan
Stefan

Reputation: 806

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
}

Upvotes: 21

Igor Kurkov
Igor Kurkov

Reputation: 5101

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
}

Upvotes: 49

unional
unional

Reputation: 15599

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.

Upvotes: 250

Related Questions