-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Regression with branded types in TypeScript 4.2 #42939
Comments
@weswigham this one might require a hotfix if we see multiple reports |
Hang on, I'm about to post an explanation of what's going on... |
TL;DR answer is to change the type Branded<T, A> = A & { __brand: T };
type BrandOf<A> = [A] extends [Branded<infer R, unknown>] ? R : never;
type BrandedValue<A> = A extends Branded<BrandOf<A>, infer R> ? R : never; This helps type inference better recognize that we're inferring between two types with identical Longer explanation... First, the example in the original post above actually works in 4.2. However, the examples in the playground link are broken as indicated, which is an effect of our improved type alias preservation. The type alias type Guid = Branded<"Guid", string>; used to simply reference the intersection type In 4.2, Some examples to illustrate the issue: type Branded<T, A> = A & { __brand: T };
type BrandOf<A> = [A] extends [Branded<infer R, unknown>] ? R : never;
type BrandedValue<A> = A extends Branded<unknown, infer R> ? R : never;
type Guid = Branded<"Guid", string>;
type T1 = BrandedValue<Branded<'GUID', string>>; // Works in 4.1 and 4.2
type T2 = BrandedValue<Guid>; // Works in 4.1, fails in 4.2
type T3 = BrandedValue<string & { __brand: "Guid" }>; // Fails in 4.1 and 4.2 With the change to |
Hey @ahejlsberg I really appreciate the workaround and the detailed explanation, it makes perfect sense. 🙏 Editor-focused improvements, despite being very welcomed, requiring code changes does feel a little like this could be a leak of implementation details. Was this really the intended behavior when trying to preserve aliases? I couldn't find mention of this behavior in #35654 or #42149 |
When you have an inference of the form type T = U extends V & infer W ? W : never; There is not one exact-right answer for For example: type Point2d = { x: number, y: number };
type Point3d = { x: number, y: number, z: number };
type T = Point3d extends Point2d & infer W ? W : never; Here, type Point3d = { x: number, y: number } & { z: number }; Then you'd see T = Or in this form, we go back to inferring the whole type: type Point3d = { x: number } & { z: number, y: number };
type T = Point3d extends Point2d & infer W ? W : never; Importantly, none of these answers are wrong. The "peeling off" heuristic behavior of intersections in inference positions is delicate and it's best to try to avoid relying on it too heavily. It's expected that indirections (including things that weren't previously considered indirections) can thwart that process. It's a nice-to-have when it works but is not a correctness deficit when it doesn't. |
The subtyping issue makes sense from a theoretical standpoint, thanks for that explanation @RyanCavanaugh. I still feel a portion of my point was missed. It's fine that there are multiple answers, but this broke code by changing which answer TypeScript picks. Since this is a hidden choice from programmers, that would be my definition of an implementation detail. I'll concede here though...I'm happy enough there's a workaround for this particular issue and to understand why it's happening. Less related to the original question, and more specific to the last paragraph regarding the "peeling off" behavior of intersections. I've seen and written several open-source libraries making pretty heavy use of this type-inference w/ intersections as a way to track requirements (as a record) that can be added/subtracted from. They all pretty much run into these issues you describe but are generally workable. Are there any proposals/features on the horizon to actually improve this ability? |
Ooh thanks for those links, I wasn't aware of either of those 🙏 But I meant more generally regarding records and not just brands, sorry I wasn't more clear. Variadic kinds from way back is the one thing that comes to mind which could have maybe solved this example like type Point3d = { x: number } & { z: number, y: number };
// I don't actually know what syntax would have worked here
type T = Point3d extends { ...Point2d, ...infer W } ? W : never; |
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
Bug Report
When attempting to put together some conditional types to extract information about branded types, TS 4.2 is no longer able to correctly infer your intent.
🔎 Search Terms
🕗 Version & Regression Information
⏯ Playground Link
Working example in 4.1.5
https://www.typescriptlang.org/play?#code/C4TwDgpgBAQgTgQwHYBMIoDwBUA0UCCAfFALwFQBkUA3lAPp0BGiqAXFFlAL4BQPokWCxQB5AGYYipKAG18AXSgQAHsAioAzrPjI0mAJZIxEOFABKeAK5IA1kgD2AdySFFAfnNR2SCADcTUPzg0Dqo6ABqCAA2lhCSxGT4SqrqKFqhehjWdk5IeIbGpmbEHmZeUD7+pnwC0ADilvoo0hnoGABEDU3teBrAcIYA5oRBgl0oGS3C4hjjI7VQ45Ex0GStKMuxs40oI0A
Not working in 4.2
https://www.typescriptlang.org/play?ts=4.2.0-beta#code/C4TwDgpgBAQgTgQwHYBMIoDwBUA0UCCAfFALwFQBkUA3lAPp0BGiqAXFFlAL4BQPokWCxQB5AGYYipKAG18AXSgQAHsAioAzrPjI0mAJZIxEOFABKeAK5IA1kgD2AdySFFAfnNR2SCADcTUPzg0Dqo6ABqCAA2lhCSxGT4SqrqKFqhehjWdk5IeIbGpmbEHmZeUD7+pnwC0ADilvoo0hnoGABEDU3teBrAcIYA5oRBgl0oGS3C4hjjI7VQ45Ex0GStKMuxs40oI0A
Not working in Nightly
https://www.typescriptlang.org/play?ts=4.3.0-dev.20210224#code/C4TwDgpgBAQgTgQwHYBMIoDwBUA0UCCAfFALwFQBkUA3lAPp0BGiqAXFFlAL4BQPokWCxQB5AGYYipKAG18AXSgQAHsAioAzrPjI0mAJZIxEOFABKeAK5IA1kgD2AdySFFAfnNR2SCADcTUPzg0Dqo6ABqCAA2lhCSxGT4SqrqKFqhehjWdk5IeIbGpmbEHmZeUD7+pnwC0ADilvoo0hnoGABEDU3teBrAcIYA5oRBgl0oGS3C4hjjI7VQ45Ex0GStKMuxs40oI0A
🙁 Actual behavior
The brand is not removed from the return type
🙂 Expected behavior
The brand should be removed from the return type
The text was updated successfully, but these errors were encountered: