Search code examples
typescriptenumsmapped-types

Why does adding parentheses remove 'readonly [x: number]: number;' from this type?


enum Animals {
  CAT,
  DOG,
  PARROT,
  SHEEP,
  SALMON
}

The following type:

type AnimalsToMappedType = {
  [K in keyof typeof Animals]: K
}

Results in:

type AnimalsToMappedType = {
  readonly [x: number]: number;
  readonly CAT: "CAT";
  readonly DOG: "DOG";
  readonly PARROT: "PARROT";
  readonly SHEEP: "SHEEP";
  readonly SALMON: "SALMON";
}

But if you add parentheses around keyof typeof Animals:

type AnimalsToMappedType = {
  [K in (keyof typeof Animals)]: K
}

The result is:

type AnimalsToMappedType = {
    CAT: "CAT";
    DOG: "DOG";
    PARROT: "PARROT";
    SHEEP: "SHEEP";
    SALMON: "SALMON";
}

QUESTION

What are the parentheses doing that removes readonly [x: number]: number;?


Solution

  • See microsoft/TypeScript#40206 for an authoritative answer.

    A mapped type of the form {[K in keyof XXX]: YYY} is considered a homomorphic mapped type, where TypeScript sees in keyof and recognizes that you're transforming some other type XXX, and can therefore use information about the original type that isn't present in the results of keyof. On the other hand, a mapped type of the similar form {[K in (keyof XXX)]: YYY} is not considered homomorphic. The parentheses cause TypeScript to first compute keyof XXX, and then the mapped types iterates over these results without knowing or remembering anything about XXX. So in keyof XXX keeps information about XXX around that in (keyof XXX) discards.

    This difference is most commonly seen when transforming a type with optional and readonly properties. A homomorphic mapped type will preserve the optional/readonly, even though such information is not visible via keyof. For example, if XXX is {a?: string, readonly b: number} then {[K in keyof XXX]: YYY} will produce {a?: YYY, readonly b: YYY}, whereas {[K in (keyof XXX): YYY} will produce {a: YYY, b: YYY}.

    In your example, you're operating on a numeric enum object, which has reverse mappings. If Animals.CAT === 0 then Animals[0] === "CAT". TypeScript models this by adding a numeric index signature to the enum object, but this index signature is suppressed when you use keyof. So while typeof Animals might look like { [k: number]: string; CAT: 0 }, keyof typeof Animals is just "CAT" and not number | "CAT". So a homomorphic mapped type over a numeric enum will have a numeric index signature, whereas a non-homomorphic mapped type over keyof a numeric enum will not.