Skip to content

Generic type gets widened in an unexpected way #52249

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Harpush opened this issue Jan 15, 2023 · 7 comments
Open

Generic type gets widened in an unexpected way #52249

Harpush opened this issue Jan 15, 2023 · 7 comments
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@Harpush
Copy link

Harpush commented Jan 15, 2023

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.

@whzx5byb
Copy link

In ts 5.0 you can just write const b = <const T>(fn: (value: T) => T, v: T): T => fn(v); (Playground).

See #51865 for details.

@Harpush
Copy link
Author

Harpush commented Jan 20, 2023

@whzx5byb It's indeed better but:

  1. You lose the original type SomethingOne and get union instead.
  2. The original problem came from rxjs types 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

@whzx5byb
Copy link

The widening behavior is intended, see #10676.

And if you want to preserve the type alias you can just add & {} just like type SomethingOne = BooleanMapToUnion<typeof isOneSomethingMap> & {}, according to #31940 (comment).

@Harpush
Copy link
Author

Harpush commented Jan 21, 2023

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.

@fatcerberus
Copy link

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.

@Harpush
Copy link
Author

Harpush commented Jan 23, 2023

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.

That's exactly what I am doing though...

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Jan 23, 2023
@RyanCavanaugh
Copy link
Member

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.

@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

No branches or pull requests

4 participants