Repository: TypeScript
Issue: Generic constraint wipes out precise key type
Labels: Help Wanted,Possible Improvement

### 🔎 Search Terms

generic, constraint, key in, key, mapped types

### 🕗 Version & Regression Information

- This is the behavior in every version I tried, and I reviewed the FAQ for entries about Mapped types



### ⏯ Playground Link

[link](https://www.typescriptlang.org/play?#code/PQKgBApgzgNglgOwC4FoAmcoEMBGMJgACSAngA7QDGATnGatPMsAgPYoCuCHUEaKANyzUoYEMABQoSLESoM2PAWLkqteikZzgOLAhSkKo8RKnAwAFQAWmMLaxgAtojiOsMMNQhlqrADRgOKxw+GhgaKwA7ghgAGa+jmAOjhyUVgEpaU6sXmCUrI5k8JRYSHxmYD6sAFYQlEhgkVYQue4eSM28YIbQSbleAI4ccF5hemFuANYEvAi8AHSmwOYAYiNQDU2ljRAA5K2xZdQBHbYAcliOEABCWADmO-sESNQkiA9IrHmscy+pSAAuCRyFqxLCUAgXK63O4AHihvQgAA8ygg0KIAEp1HJoWEbWgIO4BPQkAB8YAAvGAAN4AX3J1IkYDAy0sNlEtg6BCuHVYYUie1yLzehO6X1icCRQOZWDQaAR8MuBGRqPRYHx71JAApWPQ4D8oACaUzmWAEEqjQiANwm2kASktSphsJNzIRogAZMbTaaANrTEh2GIIgC6RupZot3WoHAItJtPtpJtJCZZ5gA6nsYB58ldAuDJmLuuynBBeWFEGAAArCBoAJhNsvlSuspXTOUmUDWCAgirzKogaNEGsJ2tdkaujquJodYARzvH7rAXsZPuZ-oggcrofDE4gRr+cdTzKTzJTEiTZnMNeoDQAjEaACIQRwGl6ld7FgiYKCxpbmAAJCAYAoagizuMsvzACCe1oShulUMBWA4BpWFiJI5yde4EIoIFkTIHIGh6MAAFEUWocEkHdCxVFhCxySpCxIBRQc1Xne5YUQWIWlI8kAH5SLAI0ewEFobQqAAZMtRB8MtWJ2MArCwUSkhiF96EDdi7gkfJfkgQpSBhKcbmwqluGzJJRBJG1WQAQTRJI5Qw8080+OwkB0t84hCUIYUpfSNJheYmwRLUIxc-cwAAImaEgAEIorAe1xNZKSkF2UQIKI5ocIINCoIlbM+HzO4Mr3Q0JGI90738sj33qajaJ6fLCt8+5zwqABlcVhACNg7CgX8IACAVoMgrZNmVJEKHqIEsEGlokFhEihncKBYRizcEoCAN8uq0kOtZCwvheOA7ggsCuQGoaRoIHtir0VguTAyIKLIUD5jZAgAAMByHb6Kl0-EsDkRSWm-UQJsCLwsE7MAeCBWIuHqfUYlerB3paWFOrSF8HD+tUsXyahcRHIlMOhdqx2ZKBcbcI0ceaNwJFnRm8e9Twyw4agYlppmsBtS8jpyn9YyQhAYEDVhKEobmOXQ0aaAgUoCFavg-MQJh908vTdDuAB5WIYVEKl0cx6gwpNVlTg5UQruwPN8jQAh5oUnhitiHJvKKtA-NwVhROlfM0CNdTDPuYK5VC8Ko02+LEvtPwTTuVg+SNNXffuJPkv-MB7LCZpchIZC8j0dUIGeJTsp+3Q0G+kqwBgVheA5JAZK8ShMGeVQgWIixbjCKk6oohqlSgGiKFhLVmvQvXDeNu1fSi2uopDc95t4W9ltWmB1rj7awF29D+9lA6UvMa4UPVDhQOv6gfEwd5JZOHLvpTvl671gbvJ7XvEIsAA4qnQepFyKUUapPaeqh8pzyNvcKAi8orvzQKvdeC0t4rQ4GtDasUD5H0sEAvkZ9-zXlrGAOsRp0xV0gLvAgkQOxQFzrZQaZ0ECfiulpIsDghC0FwPgRoDCgT4AaBKJEE8IAAGFmiUEmO8G0QMGiwONnWfyZtQKW2ZNbEsnIcoOwIE7F2kMCDuzCJ7MCGc-ZBEDiaeh1BOxGi1KI8RUi6iyNFFSMOJAgohSVGFPcRp94JztHabOdobR93bHYqAKih5gNHlccetEoEUBgfcee8C6yINsZ2VBNoN6LW3lg3eOCtpRR2pufKFhImdjrMQioABVKAn4y7CDuBwK4yAizfR8VceurtdQEV4GENyZdWA4FqPURocAOgYQlMBNAFRskVQUSVdJdwoAAGZVFvXUauNMxZbA6JmEqb4ztLJuyGXEL2FjsL+2scyJZocDJeIjj0iArYkDVK7IgCAWpAkhIvGEyq-8vlbNifVKiY9xFTxnqsuB6zMlLyWbkiQ+SME7z3rgsph8KnH1BXU1kUi9B3HYTlGCLQ4DwUrFdBRFFQZuU4UsuKzLEbIyQKjRoOyWh1mxnTfGLEhxgCJjiPEp1CQBC0rCEkB1qbqj5QzPlLMFX8w5l4JA3NeZ8sFjrDYcLjYABZtkY1AnWDR+ybbf3ticgx5zRomKueYny6tblWO1syWuTzAqvKjr4mOk5opYqSgCnOfcB6GvBSPSFCToXJIgKkg28KoD6sQSvNeeT0FLUwdgwJ5Ti7HzDQS5Y5hOoWFshiCwRpxEoFxjIz8KEQiiCLZIfChFcqkQxbCAAGgEAAmgxMAWo6LajtJSckTECaiE7WAASNUjSZOYqqUQLpmQWAkMO0dlgF2sVED26dYBZ3kJNAJQ8Jp05rQgOfGQTB5CYD4RAFAPYUQoC1kQHotN1AMFkMwNgnBuBDMEMIRhLbbxxDZRytFS1x0CrVIeddezWT9XusMr4ERVIkFOISOKjxcjVB4A0CDzSGi5jICEZ4rhK7bCuj0K25gyDCCVEcS1MYICLCTE20iZxHyVtUNW6RbiHj1t3mmZYEggA)

### 💻 Code

```ts
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/ban-types */

// This is a minimal repro, boiled down from a much, much more complicated
// project where all these types are required and make sense.

// First what we're after, this NameBag we're trying to construct:
interface NameBag<Names extends Record<string, any> = {}> {
  // This is the method we're trying to fix:
  addName<Name extends string>(options: {
    name: Name;
  }): NameBag<
    Names & {
      [key in Name]: { name: true };
    }
  >;
  // We'll come back to this method in Part 2
  addNameThatWorksFine<Name extends string>(
    name: Name
  ): NameBag<
    Names & {
      [key in Name]: { name: true };
    }
  >;
}

/// Part 1: Demonstrating the issue

// Helper to get the generic type out of a NameBag type:
export type ExtractNamesType<T> = T extends NameBag<infer E> ? E : never;

// Lets pretend we have an empty NameBag
const emptyBag: NameBag = null as any;
// And add a name to it
const filledBag = emptyBag.addName({ name: "hey!" });

// Let's get the type of the filled bag's names:
type Names1 = ExtractNamesType<typeof filledBag>;

// So far, no issue, we get what we expect:
assert<Equals<"hey!", keyof Names1>>;

// To trigger the issue, we need another wrapper. The `extends`
// constraint here is what breaks us:
function wrapper<Schema extends Record<string, NameBag>>(
  schema: Schema
): Schema {
  return schema;
}

// The issue only occurs if we create filledBag inline:
const bagOfBags = wrapper({
  // this is the same code as we used for filledBag above:
  bad: emptyBag.addName({ name: "hey!" }),
  good: filledBag,
});

// And here you can see that the `bad` bag loses its precise type:
type TBad = ExtractNamesType<(typeof bagOfBags)["bad"]>;
assert<Equals<"hey!", keyof TBad>>;

// But super surprisingly, the `good` bag is fine:
type TGood = ExtractNamesType<(typeof bagOfBags)["good"]>;
assert<Equals<"hey!", keyof TGood>>;

/// Part 2: What else works

// Assigning the NameBag to a variable works:
let fixTypeChecking;
const bagOfBags2 = wrapper({
  // this is the same code as we used for filledBag above:
  works: (fixTypeChecking = emptyBag.addName({ name: "hey!" })),
});
type TWorks2 = ExtractNamesType<(typeof bagOfBags2)["works"]>;
assert<Equals<"hey!", keyof TWorks2>>;

// Using an argument to `addName` as opposed to an object with a field
// works:
const bagOfBags3 = wrapper({
  // this is the same code as we used for filledBag above:
  works: emptyBag.addNameThatWorksFine("hey!"),
});
type TWorks3 = ExtractNamesType<(typeof bagOfBags2)["works"]>;
assert<Equals<"hey!", keyof TWorks3>>;

// Changing the generic in the constraint to NameBag works!!!:
function wrapper2<Schema extends Record<string, NameBag<any>>>(
  schema: Schema
): Schema {
  return schema;
}
const bagOfBags4 = wrapper2({
  // this is the same code as we used for filledBag above:
  bad: emptyBag.addName({ name: "hey!" }),
});
type TBad4 = ExtractNamesType<(typeof bagOfBags4)["bad"]>;
assert<Equals<"hey!", keyof TBad4>>;
```


### 🙁 Actual behavior

You can see that in certain scenarios the exact key type of the mapped type degrades back to `string`.

### 🙂 Expected behavior

The exact key type should always be preserved.

### Additional information about the issue

~~The last working version is a fine workaround that I ended up using~~ (edit: it broke things down the line, see @Andarist's proper fix until a fix comes out in the compiler), but I though I'd share the issue anyway, as the "assign to variable and get a different type" is extremely surprising for me (I also tried to find this in FAQ).