166

When I try to compile this Typescript code

interface Foo { 
    [foo: "hello" | "world"]: string;
}

I get this error message

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

What is a mapped object type, and how do I use it?

user3840170
  • 26,597
  • 4
  • 30
  • 62
Daniel Rosenwasser
  • 21,855
  • 13
  • 48
  • 61

2 Answers2

207

A mapped object type operates on a set of singleton types and produces a new object type where each of those singletons is turned into a property name.

For example, this:

type Foo = {
    [K in "hello" | "world"]: string
};

would be equivalent to

type Foo = {
    "hello": string;
    "world": string;
};

Keep in mind that a mapped object type is a distinct type operator - that syntax in the braces can't be used in interfaces, or object types with other members. For example

interface Foo {
    [K in "hello" | "world"]: string; // ❌
}

produces the following error:

A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.

Mapped object types are useful for a lot of different things. Read more here: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

GreenAsJade
  • 14,459
  • 11
  • 63
  • 98
Daniel Rosenwasser
  • 21,855
  • 13
  • 48
  • 61
  • 18
    Is there a version of this where the type can have only one property whose name must be either `hello` or `world`? – ironchicken Oct 15 '19 at 12:06
  • 1
    That's what I'm trying to figure out too: how do you make a type that has either `hello` or `world` prop and be able to narrow typing to use the correct type... – Tyler Rick Oct 23 '19 at 19:16
  • 9
    I want to point out the difference. To make a mapped type we replace the **":"** with **" in "**. And becomes `[foo in "hello" | "world"]` – mkb Jan 04 '20 at 12:20
  • 6
    Note the answer changes `interface` to `type` - this was the fix for me. – Ryan Apr 12 '20 at 17:20
  • 18
    @ironchicken, yes, you simply need to add `?` after closing `]`, more details here: https://stackoverflow.com/a/52700831/449553 – msangel May 29 '20 at 11:56
  • 7
    @ironchicken You could do `type Foo = { hello: string; world?: never; } | { world: string; hello?: never }`. You can also extend this to accept a generic union/value to make it reusable: `type Foo = OnlyOneProperty<"hello" | "world", string>`. See [playground](https://codesandbox.io/s/typescript-onlyoneproperty-72xy5?file=/index.ts) for example usage. – Daniel Thompson May 13 '21 at 19:52
  • 1
    @DanielThompson I've been searching for daaaaays for information like this. Thank you so much for linking to the playground. – kburgie Sep 24 '21 at 21:06
14

This is invalid:

    type DirectiveType = 'package-as' | 'externals' | 'move-folders' | 'ignore';
    type Directives = { [key:DirectiveType]?: RootDirectives };
    
    export class PkgmetaFile {
        private _directives: Directives = {};
    }

But this is valid:

    type DirectiveType = 'package-as' | 'externals' | 'move-folders' | 'ignore';
    type Directives = { [Key in DirectiveType as string]?: RootDirectives };
    
    export class PkgmetaFile {
        private _directives: Directives = {};
    }
ChrisKader
  • 151
  • 1
  • 5
  • 3
    Could you give more insight about your answer? Trying to understand an answer without an explanation might lead to misunderstandings. – zolastro Jan 05 '22 at 08:49
  • 2
    I was able to infer a key to a mapped object from a union type by simply using 'as string' at the end of 'DirectiveType' – ChrisKader Jan 05 '22 at 20:05
  • This is the better answer as it preserves the ability to specify a key type and have multiple interfaces use that type as a key. The accepted answer is more limited in scope and almost useless for my needs. – Jthorpe Mar 11 '22 at 19:02
  • 2
    this seems to accept any string for the key, not only DirectiveType – mrtnmgs Jan 20 '23 at 13:39
  • it also works on enums directly like this, thanks. enum example: `{[k in EMyEnum as string]: boolean}` instead of `k` for "key" you can use whatever make sense for you, and as an example it's a boolean value, but of course you want to adapt. – faebster Aug 09 '23 at 00:42
  • What @mrtnmgs says though, this removes any restrictions from the key name. – Meuko Aug 28 '23 at 14:01