microsoft / microsoft/TypeScript

Generic constraint wipes out precise key type

Aperta
#56,912 3 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Domain: Mapped Types Help Wanted Possible Improvement
Lingua principale
Go
Stelle
111k
Fork
14.4k
Merge medio
1g 19h
PR unite (30g)
117

Descrizione

🔎 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

💻 Code
/* 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).

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia con la riproduzione collegata in TypeScript Playground, prestando particolare attenzione al vincolo del wrapper e alla chiamata inline a addName che produce TBad. Traccia il modo in cui il compilatore gestisce i vincoli generici e i tipi mappati; il lavoro è completato quando la chiave esatta "hey!" viene preservata in TBad mantenendo valide le assertion esistenti.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
typescript
Ambito
compilers
Tipo di issue
Bug
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
35/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.