Typescript: String literal types as index signature parameter types?

Created on 16 Nov 2015  ·  71Comments  ·  Source: microsoft/TypeScript

According to https://github.com/Microsoft/TypeScript/pull/5185, string literal types are assignable to plain strings. Given the following type definition:

type NodeType = "IfStatement"
              | "WhileStatement"
              | "ForStatement";

Both these assignments are valid:

const nodeType1: NodeType = "IfStatement";
const nodeType2: string = nodeType1;

However, string literal types currently can't be used as index signature parameter types. Therefore, the compiler complains about the following code:

let keywords: { [index: NodeType]: string } = {
    "IfStatement": "if",
    "WhileStatement": "while",
    "ForStatement": "for"
};

// error TS1023: An index signature parameter type must be 'string' or 'number'.

Shouldn't that scenario be supported, given that string literal types are assignable to strings?

/cc @DanielRosenwasser

Literal Types Fixed Suggestion

Most helpful comment

This should be possible now using Mapped types

So you can write the example in the original example as:

type NodeType = "IfStatement"
              | "WhileStatement"
              | "ForStatement";

let keywords: {[P in NodeType]: string };

keywords = {
      "IfStatement": "if",
      "WhileStatement": "while",
      "ForStatement": "for"
};  // OK

keywords.ifStatement; // OK


keywords = {  "another": "wrong" } // Error

All 71 comments

This issue is kind of the "dual" of #2491, and if we took this on we might want to reconsider that issue.

This is something that @weswigham has brought up a few times with me. One of the issues with this sort of thing is that undefined and null are possible values for each string literal type. This isn't something we consider to be a huge problem with numeric index signatures (they suffer from the same problem), but somehow I get a feeling like this is potentially more misleading. All in all, that doesn't seem to be a strong enough reason to dismiss it though.

I'd like to argue from another perspective to point out why I think this feature is worth implementing. Consider the above NodeType type as found in a parser such as Esprima. String literal types are aimed at describing a set of finite possible string values, and listing all available statement/expression types of a parser is a perfect use case for that. If I'm forced to use plain strings as index signature parameter types, I lose the type safety that string literal types were made for to give me in the first place.

I agree, the issues are related. Let's see if we can find a solution here.

For that sort of thing, you don't necessarily need a string literal index signature - an alternative approach is to just use the appropriate property name when indexing with string literal types. It's one of the open questions on #5185:

Given that we have the textual content of a string literal type, we could reasonably perform property lookups in an object. I think this is worthwhile to consider. This would be even more useful if we performed narrowing.

Dup of #2491

@jhlange I don't entirely think it is. While we are toying with the idea of unifying literal types with enums, this is somewhat distinct right now. If we do end up bringing them together, then #2491 will become much more relevant to the discussion.

+1

There's a highly relevant discussion on this at the exact duplicate issue #7656. I suggest checking out some of the information there.

In order to truly implement something like this, the index signature parameter would first need to be type checked against the specialized type they are constrained to, e.g. { [key: number]: any } would reject a string or Symbol used as a key. Currently that in not enforced. Please see this comment in #7660 and participate in the discussion.

This comment in #7660 is highly relevant to the topic here, though refers to the more general issue of how strictly should index signature keys be type-checked.

+1

@christyharagan posted some more motivating examples in #8336.

As @malibuzios points out, membersof (#7722) would be a nice feature to pair with this, but I think that should be considered orthogonal and not discussed here.

Adding some more examples to what @mariusschulz was saying

describing a set of finite possible string values

The ability to enumerate the set of possible string values would be very useful. For example, given:

type DogName = "spike" | "brodie" | "buttercup";

interface DogMap {
  [index: DogName ]: Dog
}

let dogs: DogMap = { ... };

...it would be really nice to be able to do:

let dogNames: DogName[] = Object.keys(dogs);
// or
for (let dogName:DogName in dogs) {
  dogs[dogName].bark();
}

Here is another example of how it would be nicer if we were able to use string literal types as index keys.

Consider this interface:

interface IPerson {
  getFullName(): string;
}

If your tests this, you might write something like this:

let person: IPerson = jasmine.createSpyObj('person', ['getFullName']);

or

let person = jasmine.createSpyObj<IPerson>('person', ['getFullName']);

but suppose you messed up and instead wrote this:

let person: IPerson = jasmine.createSpyObj('person', ['foo']);

Currently there is no way for TS to let you know there is a problem. But if the type definition could look something like this:

function createSpyObj<T extends string>(baseName: string, methodNames: T[]): {[key: T]: any};

then the inferred type could be something more like { 'foo': any } which would cause an error.

Now, I'm not sure right now if the compiler would infer the type of T as a string literal union type for something like this:

let person: IPerson = jasmine.createSpyObj('person', ['foo', 'bar']); //ERROR

Ideally there would be a way to help TS infer the generic T as 'foo' | 'bar' so that the inferred return type could be { 'foo': any; 'bar': any; }.

+1

Another use case: representing valid BCP47 language tags as keys for language maps, e.g.:

{
  "@context":
  {
    "occupation": { "@id": "ex:occupation", "@container": "@language" }
  },
  "name": "Yagyū Muneyoshi",
  "occupation":
  {
    "ja": "忍者",
    "en": "Ninja",
    "cs": "Nindža"
  }
}

This would be difficult to represent, however, because there are so many valid language tags. They are not limited just the two-letter identifiers, but rather they can include things like region, e.g.:

  {
    "en-US": "elevator",
    "en-GB": "lift"
  }

Flow has this feature and it's quite nice to have.

Here's another use case: the typing of underscore's mapObject function would ideally maintain knowledge about the set of possible keys, so that:

// foo conforms to type { [key: 'x'|'y']: string }
const foo = {
  x: 'hello',
  y: 'world',
}

// type of bar is inferred to be { [key: 'x'|'y']: number }
const bar = mapObject(foo, s => s.length)

Another usage, string literal key as constraints of object.

function prop<K extends string>(name: K): <S extends { [K]: any }>(s: S) => S[K] {
    return s => s[name];
}

prop('myProp')({myProp: 123})

This pattern is heavily used in functional library like rambda.

More generally, string literal key enables dynamic type building.

Use of literal types (and more especially, generic type parameters constrained to literal types) provides a strict superset of the functionality provided in #11929. Notably:

function get<T, K extends keyof T>(obj: T, name: K): T[K] {
    return obj[name];
}
function set<T, K extends keyof T>(obj: T, name: K, value: T[K]): void {
    obj[name] = value;
}

is equivalent to, though definitely neater than:

function get<K extends (string | number), V, K1 extends K>(obj: { [key: K]: V }, name: K1): V {
    return obj[name];
}
function set<K extends (string | number), V, K1 extends K, V1 extends V>(obj: { [key: K]: V }, name: K1, value: V1): void {
    obj[name] = value;
}

Note that the additional type parameters K1 extends K, V1 extends V are necessary due to the fact that generic type parameter inference cannot be selectively disabled, so without them K and V would eventually fall back to (string | number if not {}) and ({} or any), respectively.

This should be possible now using Mapped types

So you can write the example in the original example as:

type NodeType = "IfStatement"
              | "WhileStatement"
              | "ForStatement";

let keywords: {[P in NodeType]: string };

keywords = {
      "IfStatement": "if",
      "WhileStatement": "while",
      "ForStatement": "for"
};  // OK

keywords.ifStatement; // OK


keywords = {  "another": "wrong" } // Error

Why doesn't it work with classes and interfaces? E.g.

interface Keywords {
    [P in NodeType]: string;
}

Typescript playground

@mhegazy this doenst work for parameterized types either.

interface Component<Model, Action> {
  stateful: {
    init: () => Model,
    actions: {
      [Key in Action]: ({state}: {state: Model}, event: any) => Model
    }
  }
}

Why doesn't it work with classes and interfaces? E.g.

Mapped types are only supported in type aliases. Interfaces and classes have merging semantics that do would not fit make much sense with mapped type.

@ccorcos did you mean [Key in keyof Action] ?

Or is Action a string? if so then add a constraint for Action extends string.

I tried this, but didn't work.

  type ItemName = "A" | "B" | "C";
  namespace ItemName {
    export const A: ItemName = "A";
    export const B: ItemName= "B";
    export const C: ItemName = "C";
  };

  const itemComments: {[name in ItemName]: string} = {
    [ItemName.A]: "Good",
    [ItemName.B]: "So-so",
    [ItemName.C]: "Bad",
  };

The error says:

[ts]
Type '{ [x: string]: string; }' is not assignable to type '{ A: string; B: string; C: string; }'.
  Property 'A' is missing in type '{ [x: string]: string; }'.

I think [ItemName.A] should be recognized as ItemName type, not a string.

I think [ItemName.A] should be recognized as ItemName type, not a string.

This is the same issue tracked by https://github.com/Microsoft/TypeScript/issues/5579.

@mhegazy If this doesn't work in interfaces, is there a different solution available, or a tracking issue for that case?

I'm trying to use this in some type definitions, and it doesn't support my case, using classes (or interfaces):

type ValidKey = "a" | "b" | "c";

declare class MyClass {
  [key: k in ValidKey]: string
}

I get "A parameter initializer is only allowed in a function or constructor implementation" (which isn't a particularly great error either, even given that this is unsupported).

I found out that in order to make keys optional you can use Partial with this e.g.

type mapping = Partial<{ [k in "a" | "b"]: string }>

Small thing, but I struggled for a while with this.

P.S. I probably should read specs more closely, there is also a { [k in "a" | "b"] ? : string } for optional keys.

It'd be great if this also worked for number literals:

{ [k in 1 | 2]: string }

Since this already works:

{ [k: number]: string }

@leoasis I seem to recall that when mapped types were first introduced they did support other literals than strings, but that support was subsequently removed. I wonder why...

Any update on this?

If I understand the thread correctly, is it not possible to parametrize a class by a type, then use the keys of that type in an index signature, like so:

class Form<T> {
    initialValues: {[fieldName: keyof T]: string} = {}
}

@MaxGabriel Did you mean something like this?

class Form<T> {
  constructor(readonly initialValues: Partial<{ [field in keyof T]: string }> = {}) {}
}

@karol-majewski Oh that works great! Thank you :)

I came here when I ran into the error for enums. This is how I made it work:

export enum ThreeLetters {
  a = 'a',
  b = 'b',
  c = 'c'
}

const letterToName: {[key in ThreeLetters]: string} = {
  a: 'Ashok',
  b: 'Bobby',
  c: 'Clever'
}

String literal types compatible with index signatures is one of the features I miss in TypeScript since a long time.

So often I define string literal enums in my project, such as:

type EmployeeType = "contractor" | "permanent";

or even list of geo, locale-related or money-related info, such as:

type Currency = "USD" | "GBP" | "EUR";

(whenever the number of items is fixed, not just a string).

Given a collection of items which include a property of above types, when reducing or grouping by that property, I expect to get:

{
  "contractor": ReducedValue...,
  "permanent": ReducedValue...
}

and I can't define the type as:

{ [type: EmployeeType]: ReducedType }

because it can be only a string or a number. So only thing I can do is to broaden it to a string, which is in fact to broad, since "other" is not a part of EmployeeType.

I wish this small feature lands in TypeScript one day :) not really sure why a string literal enum - being a subtype of string - is not allowed as an index signature (and string, the broader one, is).

@ducin can you help me understand your use case better? The following seems to work as (I) expect:

type EmployeeType = "contractor" | "permanent";

const employees: { [employeeType in EmployeeType]: {} } = {
  "contractor": {}, // ok
  "permanent": {}, // ok
  "volunteer": {} // error
}

@niedzielski I'm ashamed I never knew about this way to define an index signature. Thank you for this hint!

BTW why isn't { [type: employeeType]: {} } allowed (i.e. in is allowed and : isn't, what's the difference)?

in makes a mapped type, while the other is an index signature. Mapped types are a little different in that they cannot have any properties other than the mapping operation (and can't be used in interfaces).

@niedzielski What if I want any subset of string literal type be ok. Can I do this in 2018?

@vehsakul You mean like this?

type MyStringLiteral =
  | "a"
  | "b"
  | "c";

const asRecordType: Partial<Record<MyStringLiteral, any>> = {
    b: 1,
}

const asMappedType: { [property in MyStringLiteral]?: any } = {
    b: 1,
}

@karol-majewski Yes, exactly. Thanks. Dropped out from TS world for a while. Now ask dumb questions :)

Not sure if I'm missing something here, but I would've expected this to work.

type Department = "finance" | "engineering" | "marketing";
type DepartmentMap<TValue> = { [P in Department]: TValue };

let employeesByDepartment: DepartmentMap<string[]> = {
    "finance": ["Karen", "Jack"],
    "engineering": ["Natalie", "Matt"],
    "marketing": ["Michael", "Sally"]
};

for (let department in employeesByDepartment) {
    // Type of department is string, so:
    // Why am I allowed to use it as an indexer on line 13? Shouldn't I only be allowed to index with a Department?
    // Why is it not of type Department?
    let employees: string[] = employeesByDepartment[department];
    assignDepartmentInPayroll(department, employees); // this line does not compile
}

function assignDepartmentInPayroll(department: Department, employees: string[]) {
    // do something
}

I have a feeling there's a right way to do this, but I can't seem to find it.

@MRayermannMSFT Type assertion (department as Department, employees) does the job.

In my opinion above code is semantically correct, just the for..in is unable to narrow down the type of the key to a string literal enum. Such internal incompatibilities in TypeScript do appear, unfortunately. Maybe such issue could be easily improved, @mhegazy ?

See https://github.com/Microsoft/TypeScript/pull/12253#issuecomment-263132208 for why department can't be inferred to have type Department. Having exact types might solve the problem?

Yep that would explain the reason why the type is not inferred. I'll look into exact types, @ducin , Yes, I could use as to get the property type, but to me that's not the ideal solution. Thank you for the suggestion though, as it is what I'll probably have to go with. 😄

A related question, I wonder why:
Given:

type NodeType = "IfStatement"
              | "WhileStatement"
              | "ForStatement";

The code below works:

type GoodStuff = {[P in NodeType]?: string } // Note that value type is set to "string"
let keywords: GoodStuff = {
      IfStatement: "if",
      WhileStatement: "while",
      another: "another", // This triggers error as expected
}; 

But this doesn't:

type GoodStuff = {[P in NodeType]?: NodeType } // Note that value type is set to "NodeType"
let keywords: GoodStuff = {
      IfStatement: "if",
      WhileStatement: "while",
      another: "another", // This no longer triggers error :(
}; 

@benjamin21st It does seem to work correctly. See TypeScript playground with TypeScript 3.0.1 installed.

@karol-majewski Ah~ Thanks! Then I'll just have to convince my team to upgrade to 3.0 :joy:

If you have an enum (indexed by numbers), but you want your object to have string keys, here’s my solution:

enum NodeEnum {
    IfStatement,
    WhileStatement,
    ForStatement,
}

const keywords: { [index in keyof typeof NodeEnum]: string } = {
    "IfStatement": "if",
    "WhileStatement": "while",
    "ForStatement": "for",
}

Actually you can write this like the following:

enum Color {
    red,
    green,
    blue
}

type ColorMap<C extends object, T> = { [P in keyof C]: T };

const colors: ColorMap<typeof Color, string> = {
    red: 'red',
    green: 'green',
    blue: 'blue'
};

Boom type checking!

Actually you can write this like the following:

enum Color {
    red,
    green,
    blue
}

type ColorMap<C extends object, T> = { [P in keyof C]: T };

const colors: ColorMap<typeof Color, string> = {
    red: 'red',
    green: 'green',
    blue: 'blue'
};

Boom type checking!

Thanks!!! This is AWESOME!!!

enum Color { red, green, blue } type ColorMap = { [P in keyof C]: T }; const colors: ColorMap = { red: 'red', green: 'green', blue: 'blue' };

Not really, if you change your colors to be:

const colors: ColorMap<typeof Color, string> = {
    red: 'green', // <------- We don't expect this, do we?
    green: 'green',
    blue: 'blue'
};

Same situation

type Method = 'get' | 'post' | 'put' | 'delete'
const methods: Method[] = ['get', 'post', 'put', 'delete']
class Api {
  [method in Method]: Function // <-- Error here
  constructor() {
    methods.forEach(method => {
      this[method] = (options: any) => { this.send({ ...options, method }) }
    })
  }
  send(options:any) {
    // ....
  } 
}

How should i handle this case ?

For Googlers and others

What you want (and does NOT work):

type Ma = { [key: 'Foo']: number }
type Mb = { [key: 'Foo' | 'Bar']: number }
type Mc = { [key: 'Foo' | 'Bar' | 0.3]: number }
// etc

What you need (and does work):

type Ma = { [key in 'Foo']?: number }
type Mb = { [key in 'Foo' | 'Bar']?: number }
type Mc = { [key in 'Foo' | 'Bar' | 0.3]?: number }

const x: Ma = {}
const y: Ma = { 'Foo': 1 }
const z: Mc = { [0.3]: 1 }
// const w: Ma = { 'boop': 1 } // error

Unfortunate constraints:
Example 1

type Mx = {
  Bar: number // OK, but has to be a number type
  [key: string]: number
}

Example 2

type My = {
  Bar: number, // Error, because of below
  [key in 'Foo']: number
}

Is it possible to achieve something like this:

// TPropertyName must be a string
export type Foo<TPropertyName = "propertyName"> = {
  [key in TPropertyName]: number
};

@n1ru4l You need to set a constraint on TPropertyName:

export type Foo<TPropertyName extends string = "propertyName"> = {
  [key in TPropertyName]: number
};

Ok, with all these solutions proposed, it seems there is still none piece of the puzzle missing; iterating over an object's keys:

declare enum State {
  sleep,
  idle,
  busy,
}

type States = { [S in keyof typeof State]: number };

const states: States = {
  sleep: 0x00,
  idle: 0x02,
  busy: 0x03,
};

function getNameFromValue(state: number): State | undefined {
  for (const k in states){
    if (states[k] === state) {
      return k; // type string !== type State and cannot be typecasted
    }
  }
}

The solution @dcousens proposed doesn't really help because my State enum is actually 20 lines of code and I don't think anyone would want that in their project.

@LukasBombach What do you want getNameFromValue to return — the name of one of the keys or the numeric value on the right-hand side of your enum?

  • State refers to the _value_ of an enum,
  • typeof State would be the type of your enum (here: an object),
  • keyof typeof State is the key of your enum.

If your enum looks like this:

enum State {
  sleep = 0x00,
  idle = 0x02,
  busy = 0x03,
}

Then you can get the key by doing:

function getNameFromValue(state: number): keyof typeof State | undefined {
  return State[state] as keyof typeof State | undefined;
}

and the value by doing:

function getNameFromValue(state: number): State | undefined {
    for (const k of UNSAFE_values(State)) {
      if (state === k) {
        return k
      }
    }

    return undefined;
}

const UNSAFE_values = <T extends object>(source: T): T[keyof T][] =>
  Object.values(source) as T[keyof T][];

@karol-majewski thank you! What I want to return is the String that is restricted to specific values, I managed to do it the way I do it up there. The way I understand your solution, it is similar to mine but the keys and values / the access to it is reversed.

What bugs me is that I have to do a type cast, which I'd like to avoid.

I've looked over this thread, and I'm a little confused. Why does this work:

type Point<D extends string> = {
  [key in D]: number;
}

but this does not?

interface Point<D extends string> { 
  [key in D]: number 
}

image

It seems to me that the two should be equivalent. What am I missing?

Perhaps you could post an example of what you're after here as what you're showing here is wanting each key in a string. Not typical.

If you have a point that has say x and y

const point = {
  x: 100,
  y: 200
}

Then you'd have a type something like this:

interface IPoint {
  x: number;
  y: number;
}
type PointKeys = keyof IPoint;

But again maybe post a little more of what you're after here.

Or maybe you're after something like this:

interface IPoint {
  x: number;
  y: number;
}

const points = {
  one: { x: 100, y: 200 },
  two: { x: 200, y: 300 }
};

type PointKeys = keyof typeof points;

type Points = { [K in PointKeys]: IPoint };

What I've been trying to express is a Point with an arbitrary number of named dimensions. For example:

const point2D: Point<'x' | 'y'> = {x: 2, y: 4};
const point6D: Point<'u' | 'v' | 'w' | 'x' | 'y' | 'z'> = {
  u: 0,
  v: 1,
  w: 2,
  x: 3,
  y: 4,
  z: 5
};

But I think my use case isn't as important as the question of why the index signature works in a type alias but not in an interface?

I just spent a long time trying to get it to work as an interface before realizing that the same thing as a type alias works. It's a little confusing why one would work but not the other.

I see, I misunderstood you're not asking for a solution but the why?

So this works just fine, I'm assuming you realized that but to be clear:

type Point<Keys extends string> = { [K in Keys]: number };

const point2D: Point<'x' | 'y'> = {x: 2, y: 4};

const point6D: Point<'u' | 'v' | 'w' | 'x' | 'y' | 'z'> = {
  u: 0,
  v: 1,
  w: 2,
  x: 3,
  y: 4,
  z: 5
};

Unlike the type alias which is enumerating the keys an interface is a definition hence the generic type would have to be an object or a Symbol. So what you're trying to do here needs to be done with a type alias as you're not defining it but rather representing what it is based on the keys. Think of it like a Record<T, K extends string> if that makes sense.

I think index signature parameter should also allow for the String type and sub-types because it is valid. I need this for the scenario I've explained here: https://github.com/microsoft/TypeScript/issues/6579#issuecomment-537306546

@mariusschulz, how about let keywords: { [key in keyof NodeType]: string }?

@mariusschulz, how about let keywords: { [key in keyof NodeType]: string }?

I don't think it makes sense, keyof NodeType will give you different literal strings - methods on String type.

What I tend to do is to reverse the problem, usually it's enough for my cases:

interface KeywordsMappings  {
  IfStatement: "if", // or string if you want to widen the type
  WhileStatement: "while",
  ForStatement: "for"
}

type Keywords = keyof KeywordsMappings

let keywords: KeywordsMappings = {
    "IfStatement": "if",
    "WhileStatement": "while",
    "ForStatement": "for"
};

Not sure what happens but I guess it's a similar problem:

type TestMap<T extends string> = {[key in T]: string}

const a = <T extends string>(aa: T) => {
    const x: TestMap<T> = {
        [aa]: 'string'
    }
}

//Type '{ [x: string]: string; }' is not assignable to type 'TestMap<T>'.(2322)

We can achieve this by using Record :

type NodeType = 'IfStatement' | 'WhileStatement' | 'ForStatement'
type NodeTypeObject = Record<NodeType, string>

// works!
var myNodeTypeObject: NodeTypeObject = {
  IfStatement: 'if',
  WhileStatement: 'while',
  ForStatement: 'for',
}

// complains if there are missing proeprties
var myNodeTypeObject: NodeTypeObject = {
  IfStatement: 'if',
  WhileStatement: 'while',
} // --> Error : Property 'ForStatement' is missing but required by type 'Record<NodeType, string>'.

// Complains if additional properties are found
var myNodeTypeObject: NodeTypeObject = {
  IfStatement: 'if',
  WhileStatement: 'while',
  ForStatement: 'for',
  foo :'bar'  // --> Error :  'foo' does not exist in type 'Record<NodeType, string>'.
}

Bonus: If we want the properties to be optional we can do it by using Partial:

type NodeType = 'IfStatement' | 'WhileStatement' | 'ForStatement'
type NodeTypeObject = Partial<Record<NodeType, string>>

// works!
var myNodeTypeObject: NodeTypeObject = {
  IfStatement: 'if',
  WhileStatement: 'while',
}

try it in the typescript playground

But in this solution your cannot iterate the keys:

type NodeType = 'IfStatement' | 'WhileStatement' | 'ForStatement'
type NodeTypeObject = Record<NodeType, string>

// works
var myNodeTypeObject: NodeTypeObject = {
  IfStatement: 'if',
  WhileStatement: 'while',
  ForStatement: 'for',
}

function getNameFromValue(str: string): NodeType | undefined {
  for (const k in myNodeTypeObject){
    if (myNodeTypeObject[k] === str) { // any
      return k; // is a string
    }
  }
}

Playground

https://github.com/microsoft/TypeScript/issues/5683#issuecomment-515744911

Was this page helpful?
0 / 5 - 0 ratings

Related issues

uber5001 picture uber5001  ·  3Comments

dlaberge picture dlaberge  ·  3Comments

seanzer picture seanzer  ·  3Comments

kyasbal-1994 picture kyasbal-1994  ·  3Comments

siddjain picture siddjain  ·  3Comments