TypeScript
TypeScript copied to clipboard
Generic type gets widened in an unexpected way
Bug Report
🔎 Search Terms
widen
🕗 Version & Regression Information
4.9.4
⏯ Playground Link
Playground link with relevant code
💻 Code
enum One {
A = 'a',
B = 'b',
C = 'c'
}
const isOneSomethingMap = {
[One.A]: true,
[One.B]: false,
[One.C]: true
} as const satisfies Record<One, boolean>;
type BooleanMapToUnion<T extends Record<string, boolean>> = {
[P in keyof T]: T[P] extends true ? P : never;
}[keyof T];
type SomethingOne = BooleanMapToUnion<typeof isOneSomethingMap>;
const a = <T>(value: T) => value;
const b = <T>(fn: (value: T) => T, v: T): T => fn(v);
const v: SomethingOne = One.A as SomethingOne;
const v2: One.A | One.C = One.A as One.A | One.C;
const r1 = b(a, v); // One and not SomethingOne - not expected
const r2 = a(v); // SomethingOne as expected
const r3 = b(a, v2); // Union as expected
const r4 = b(a, One.A as One.A | One.C); // Works
const r5 = b(a, One.A as SomethingOne); // Still doesn't work...
🙁 Actual behavior
The type gets widened when using BooleanMapToUnion... I guess it is the culprit.
🙂 Expected behavior
Both union and using BooleanMapToUnion should work equally.
In ts 5.0 you can just write const b = <const T>(fn: (value: T) => T, v: T): T => fn(v); (Playground).
See https://github.com/microsoft/TypeScript/pull/51865 for details.
@whzx5byb It's indeed better but:
- You lose the original type
SomethingOneand get union instead. - The original problem came from
rxjstypes which I have zero control over.
Adding another weird case and easier one (again works for union and not BooleanMapToUnion):
const e = <T>(value: T): {a: T} => ({a: value});
const g = e(v); // Gets widened
const e2 = <T>(value: T): T => value;
const g2 = e2(v); // Works
The widening behavior is intended, see https://github.com/microsoft/TypeScript/pull/10676.
And if you want to preserve the type alias you can just add & {} just like type SomethingOne = BooleanMapToUnion<typeof isOneSomethingMap> & {}, according to https://github.com/microsoft/TypeScript/issues/31940#issuecomment-841712377.
With all respect to the new const T feature I think using it here just masks the bug.
There is no reason using type SomethingOne = One.A | One.C will behave correctly while BoooleanMapToUnion won't.
const T will force specific types and probably libraries like rxjs won't change everything to it thus the problem remains.
Considering & {} - Seems like that solves it even without const T... Thats certainly weird though.
I’m pretty sure that BooleanMapToUnion<typeof isOneSomethingMap> doesn’t actually produce One.A | One.C. Setting a property to never in the mapped type doesn’t remove it. You need to use an as clause to rename the key to never to remove it.
I’m pretty sure that
BooleanMapToUnion<typeof isOneSomethingMap>doesn’t actually produceOne.A | One.C. Setting a property toneverin the mapped type doesn’t remove it. You need to use anasclause to rename the key toneverto remove it.
That's exactly what I am doing though...
I'd agree something weird appears to be happening here. A demonstration is to slightly tweak these definitions
const b = <T>(fn: null | ((value: T) => T), v: T): T => fn!(v);
const r1 = b(null, v); // SomethingOne, as expected
The call of b(a, v) where a is the identity function shouldn't have changed the inference to One here; I don't see a good reason for that to have happened.