-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Open
Labels
Domain: Mapped TypesThe issue relates to mapped typesThe issue relates to mapped typesHelp WantedYou can do thisYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some casesThe current behavior isn't wrong, but it's possible to see that it might be better in some cases
Milestone
Description
π 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
π» 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).
Metadata
Metadata
Assignees
Labels
Domain: Mapped TypesThe issue relates to mapped typesThe issue relates to mapped typesHelp WantedYou can do thisYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some casesThe current behavior isn't wrong, but it's possible to see that it might be better in some cases