Support number and symbol named properties with keyof and mapped types

TypeScript 2.9 adds support for number and symbol named properties in index types and mapped types.Previously, the keyof operator and mapped types only supported string named properties.

Changes include:

  • An index type keyof T for some type T is a subtype of string | number | symbol.
  • A mapped type { [P in K]: XXX } permits any K assignable to string | number | symbol.
  • In a for…in statement for an object of a generic type T, the inferred type of the iteration variable was previously keyof T but is now Extract<keyof T, string>. (In other words, the subset of keyof T that includes only string-like values.)Given an object type X, keyof X is resolved as follows:

  • If X contains a string index signature, keyof X is a union of string, number, and the literal types representing symbol-like properties, otherwise

  • If X contains a numeric index signature, keyof X is a union of number and the literal types representing string-like and symbol-like properties, otherwise
  • keyof X is a union of the literal types representing string-like, number-like, and symbol-like properties.Where:

  • String-like properties of an object type are those declared using an identifier, a string literal, or a computed property name of a string literal type.

  • Number-like properties of an object type are those declared using a numeric literal or computed property name of a numeric literal type.
  • Symbol-like properties of an object type are those declared using a computed property name of a unique symbol type.In a mapped type { [P in K]: XXX }, each string literal type in K introduces a property with a string name, each numeric literal type in K introduces a property with a numeric name, and each unique symbol type in K introduces a property with a unique symbol name.Furthermore, if K includes type string, a string index signature is introduced, and if K includes type number, a numeric index signature is introduced.

Example

  1. const c = "c";
  2. const d = 10;
  3. const e = Symbol();
  4. const enum E1 { A, B, C }
  5. const enum E2 { A = "A", B = "B", C = "C" }
  6. type Foo = {
  7. a: string; // String-like name
  8. 5: string; // Number-like name
  9. [c]: string; // String-like name
  10. [d]: string; // Number-like name
  11. [e]: string; // Symbol-like name
  12. [E1.A]: string; // Number-like name
  13. [E2.A]: string; // String-like name
  14. }
  15. type K1 = keyof Foo; // "a" | 5 | "c" | 10 | typeof e | E1.A | E2.A
  16. type K2 = Extract<keyof Foo, string>; // "a" | "c" | E2.A
  17. type K3 = Extract<keyof Foo, number>; // 5 | 10 | E1.A
  18. type K4 = Extract<keyof Foo, symbol>; // typeof e

Since keyof now reflects the presence of a numeric index signature by including type number in the key type, mapped types such as Partial<T> and Readonly<T> work correctly when applied to object types with numeric index signatures:

  1. type Arrayish<T> = {
  2. length: number;
  3. [x: number]: T;
  4. }
  5. type ReadonlyArrayish<T> = Readonly<Arrayish<T>>;
  6. declare const map: ReadonlyArrayish<string>;
  7. let n = map.length;
  8. let x = map[123]; // Previously of type any (or an error with --noImplicitAny)

Furthermore, with the keyof operator’s support for number and symbol named keys, it is now possible to abstract over access to properties of objects that are indexed by numeric literals (such as numeric enum types) and unique symbols.

  1. const enum Enum { A, B, C }
  2. const enumToStringMap = {
  3. [Enum.A]: "Name A",
  4. [Enum.B]: "Name B",
  5. [Enum.C]: "Name C"
  6. }
  7. const sym1 = Symbol();
  8. const sym2 = Symbol();
  9. const sym3 = Symbol();
  10. const symbolToNumberMap = {
  11. [sym1]: 1,
  12. [sym2]: 2,
  13. [sym3]: 3
  14. };
  15. type KE = keyof typeof enumToStringMap; // Enum (i.e. Enum.A | Enum.B | Enum.C)
  16. type KS = keyof typeof symbolToNumberMap; // typeof sym1 | typeof sym2 | typeof sym3
  17. function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  18. return obj[key];
  19. }
  20. let x1 = getValue(enumToStringMap, Enum.C); // Returns "Name C"
  21. let x2 = getValue(symbolToNumberMap, sym3); // Returns 3

This is a breaking change; previously, the keyof operator and mapped types only supported string named properties.Code that assumed values typed with keyof T were always strings, will now be flagged as error.

Example

  1. function useKey<T, K extends keyof T>(o: T, k: K) {
  2. var name: string = k; // Error: keyof T is not assignable to string
  3. }

Recommendations

  • If your functions are only able to handle string named property keys, use Extract<keyof T, string> in the declaration:
  1. function useKey<T, K extends Extract<keyof T, string>>(o: T, k: K) {
  2. var name: string = k; // OK
  3. }
  • If your functions are open to handling all property keys, then the changes should be done down-stream:
  1. function useKey<T, K extends keyof T>(o: T, k: K) {
  2. var name: string | number | symbol = k;
  3. }
  • Otherwise use —keyofStringsOnly compiler option to disable the new behavior.