- Notifications
You must be signed in to change notification settings - Fork 13.2k
Structural tag type brands#33290
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
Structural tag type brands #33290
Uh oh!
There was an error while loading. Please reload this page.
Conversation
weswigham commented Sep 6, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
aa08d39 to ddf4c06Compareweswigham commented Sep 6, 2019
@typescript-bot pack this just cuz |
typescript-bot commented Sep 6, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Heya @weswigham, I've started to run the tarball bundle task on this PR at ddf4c06. You can monitor the build here. It should now contribute to this PR's status checks. |
typescript-bot commented Sep 6, 2019
Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running |
AnyhowStep commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
If one wanted the same behavior as the other PR (nominally typed, different libraries are incompatible, different versions of same library are incompatible), would it be done like this? typeNormalizedPath=string&tagunique symbol;typeAbsolutePath=string&tagunique symbol;If so, this PR basically has all the features of the other PR and more. |
weswigham commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Not quite - declarevarnormalizedSym: unique symbol;typeNormalizedPathBrand=tag{[normalizedSym]: void;};typeNormalizedPath=string&NormalizedPathBrand;declarevarabsoluteSym: unique symbol;typeAbsolutePathBrand=tag{[absoluteSym]: void;};typeAbsolutePath=string&AbsolutePathBrand;or with a hypothetical declarevarnormalizedSym: unique symbol;typeNormalizedPath=string&Tag<typeofnormalizedSym>;declarevarabsoluteSym: unique symbol;typeAbsolutePath=string&Tag<typeofabsoluteSym>; |
AnyhowStep commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
I was under the impression that this would work, declarevarnormalizedSym: unique symbol;typeNormalizedPath=string&tagtypeofnormalizedSym;Since,
Is there a reason to favor the declarevarnormalizedSym: unique symbol;typeNormalizedPath=string&tag{[normalizedSym]: void;}; |
fatcerberus commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
So after my initial knee-jerk reaction favoring
Edit: Allowing the tag properties to be typed enables phantom types to be expressed very easily; ignore the above paragraph. |
fatcerberus commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
On the other hand, if the type of the tag properties is not artificially limited to So in case it wasn’t obvious: I retract my previous suggestion of dropping the types from the tag literal. 😉 |
leemhenson commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
One of the rough edges with existing branding techniques like this is that the printed type is pretty noisy once you have multiple brands assigned at the same time. For example, using id: t.Branded<t.Branded<string,NonEmptyStringBrand>,UuidBrand>When you use a lot of these sorts of types, you can end up with compound types that are difficult to read. Would this PRs approach help in this regard? The other PR intuitively seems to suggest that if I create a nominal type along the lines of typeNonEmptyString= unique string;typeUuid= unique string;typeUserId=NonEmptyString&Uuid;then I'd just see id: UserId |
AnyhowStep commented Sep 7, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
They can "fix" that nesting by changing the definition of exporttypeBranded<A,B>=[A&Brand<B>][0]This should then give you, id: string&Brand<NonEmptyStringBrand>&Brand<UuidBrand>So, the problem isn't a problem with branding in particular |
jack-williams commented Sep 9, 2019
Small Q: What is the planned behaviour of the following? typeGetTag<T>=Textendstag infer U ? U : neverRight now I don't think it was working, but is the plan for it to infer the tag as one would expect? Is there strong motivation for having tags be in the domain of types, beyond implementation? Clearly you get a huge amount of the implementation for free, but I wonder if the user experience in the most general case is negatively impacted. As an example, it would seem natural, or at least satisfy most requirements, to write : typeNormalizedPath=string&tag"NormalizedPath";typeAbsolutePath=string&tag"AbsolutePath";typeNormalizedAbsolutePath=NormalizedPath&AbsolutePath;declarefunctionisNormalizedPath(x: string): x is NormalizedPath;declarefunctionisAbsolutePath(x: string): x is AbsolutePath;declarefunctionconsumeNormalizedAbsolutePath(x: NormalizedAbsolutePath): void;constp="/a/b/c";if(isNormalizedPath(p)){if(isAbsolutePath(p)){consumeNormalizedAbsolutePath(p);}}however here the tag of The intended way to write this: Are there use-cases where having tags be a type is either very intuitive, or capable of expressing some clearly desirable pattern? I'm not yet convinced GADT's fallout of this nicely, but I'm more than happy to be corrected. I'm sure someone will come along with an incredibly detailed implementation of units-of-measure, but these complicated conditional types are really hard to debug and understand. So I guess my point is here: if we are assuming a blank slate to implement a new feature with new syntax, is this approach the best approach for most cases most of the time? Or as an open challenge:
I'm not really sure what is better right now, I'm just curious to understand the design space more and see how people would use it. |
AnyhowStep commented Sep 9, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
I hadn't considered the
F#'s implementation is so good
Starts to write example
Ah. Nevermind. One such complicated example that would benefit from structural tags, |
weswigham commented Sep 9, 2019
Yeah, that should work (at least I don't see why it would not). Thanks for the report - I think because I have
Phantom types, mostly. Much like with our implementation of mapped types and conditional types, I think having a general underlying mechanism that enables multiple usecases, but also presents with a simple interface for common cases (like
Most certainly not possible. A key feature of units of measure is composition over algebraic operators (imo), which this does not enable. So while you could track units, you can't manipulate them easily, so... eh.
Some of the functional utility libraries I've read ( |
jack-williams commented Sep 9, 2019
They are fair points, but just to debate the pointfor the sake of it: I think solving multiple use cases is only one aspect: it's probably worth evaluating it against the distribution of uses cases and how well each one is addressed. What is the magnitude of the improvement for the phantom type use case? Not having ghost members is useful, as is an optimised representation, but if you're doing anything non-trivial you'll still be doing complex type logic or relying on casts to push through generic constraints that are hard to verify. Not to mention that phantom types are never really going to be that useful unless you have the associated constraints discharged on 'pattern matching', IMO. Conversely, it seems like the majority of users really do want branding (at least from the examples in associated issues) and there is the opportunity to deliver first-class support for that feature. Though, if I'm being honest, if there was new logic added for type display such that string&Tag<"NormalizedPath"|"AbsolutePath">rather than: string&tag({NormalizedPath: void}&{AbsolutePath: void})it would probably be fine. And FWIW, I prefer this to This was the units-of-measure implementation I was thinking of: https://github.com/jscheiny/safe-units. |
weswigham commented Sep 9, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
@jack-williams I've done one better - not only are instances of the global Also tags now distribute over unions, because I neglected that in the first pass, and it really doesn't seem to make as much sense if they don't. This means |
5952f5c to b38d95fCompareweswigham commented Sep 9, 2019
@typescript-bot pack this again now that we do inference, unions, and aliases better |
typescript-bot commented Sep 9, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Heya @weswigham, I've started to run the tarball bundle task on this PR at b38d95f. You can monitor the build here. It should now contribute to this PR's status checks. |
typescript-bot commented Sep 9, 2019
Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running |
joshburgess commented Sep 10, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
This looks interesting and syntactically reminds me of how the library typeKey<A>=string&tag{Key: A}, is currently implemented like the following in interfaceKey<A>extendsNewtype<{readonlyKey: unique symbol,readonlyphantom: A},string>{}although the interfaceNewtype<URI,A>{readonly_URI: URIreadonly_A: A}While this gives you the power to express phantom types by altering the nominal type via a structural means, I do think it sort of leaks implementation details that the user shouldn't necessarily need to care about. It's definitely better than not being able to do this at all, but I think it would be more ideal if such structural modification was done behind the scenes without the user needing to explicitly modify the structure of a tag. |
fatcerberus commented Sep 10, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
🎉 🎊 YES! I was starting to think I was the only one who saw the value of these, but they're a bit tricky to represent under the current realization of structural typing... |
jack-williams commented Sep 10, 2019
@weswigham Those baselines look really nice now. Would it be possible for an interface to extend a tag type? interfaceParentextendsTag<"A">{}interfaceChild1extendsParent,Tag<"B">{}interfaceChild2extendsParent,Tag<"C">{} |
chriskrycho commented Sep 11, 2019
I tried pulling that tgz and using it locally and it failed… did I do something wrong? |
weswigham commented Sep 11, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Most slashes have some kind of meaning in a shell, so |
weswigham commented Sep 18, 2019
@typescript-bot pack this |
typescript-bot commented Sep 18, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Heya @weswigham, I've started to run the tarball bundle task on this PR at cc8764f. You can monitor the build here. It should now contribute to this PR's status checks. |
typescript-bot commented Sep 18, 2019
Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running |
weswigham commented Sep 18, 2019
@typescript-bot pack this one last time just to check if lints and scripts do work now |
typescript-bot commented Sep 18, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Heya @weswigham, I've started to run the tarball bundle task on this PR at cc8764f. You can monitor the build here. It should now contribute to this PR's status checks. |
typescript-bot commented Sep 18, 2019
Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running |
weswigham commented Sep 18, 2019
Alright, sorry for the spam for anyone watching - I had to debug a small issue with the build, but the tarball should be functional again. |
chriskrycho commented Sep 18, 2019
Thanks for getting it straightened out! |
typescript-bot commented Sep 18, 2019
It looks like you've sent a pull request to update our 'lib' files. These files aren't meant to be edited by hand, as they consist of last-known good states of the compiler and are generated from 'src'. Unless this is necessary, consider closing the pull request and sending a separate PR to update 'src'. |
ProdigySim commented Sep 18, 2019 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Would it be possible to support Record/index types with either of these proposals? A common pattern in redux involves creating a "normalized" store of objects, where objects are stored in maps indexed on unique identifiers. Since redux stores are POJOs, we can't simply use Here's some simple test code that currently errors on the experimental build: typeUserId=string&Tag<'unique-userid'>;constuserRec: Record<UserId,string>={};constuserMap: {[idx: UserId]: string;}={};declareconstuserId: UserId;userRec[userId]='bob';console.log(userRec[userId]);userMap[userId]='bob';console.log(userMap[userId]);It seems like if the |
typescript-bot commented Sep 18, 2019
It looks like you've sent a pull request to update our 'lib' files. These files aren't meant to be edited by hand, as they consist of last-known good states of the compiler and are generated from 'src'. Unless this is necessary, consider closing the pull request and sending a separate PR to update 'src'. |
1 similar comment
typescript-bot commented Sep 18, 2019
It looks like you've sent a pull request to update our 'lib' files. These files aren't meant to be edited by hand, as they consist of last-known good states of the compiler and are generated from 'src'. Unless this is necessary, consider closing the pull request and sending a separate PR to update 'src'. |
weswigham commented Sep 18, 2019
You'd just need #26797 (with either) |
ProdigySim commented Oct 9, 2019
Another minor difference I thought of today while debugging an issue with a manual You can try this code against the test build the bot created: // Experimental Tag Types from this PRtypeBuiltin=string&Tag<'builtin'>;// truetypeyy=Builtinextendsstring ? true : false;// falsetypezz=Builtinextendsobject ? true : false;// DIY tag type solutiontypeRetroTag<T,K>=T&{__tag: K}typeRetro=string&RetroTag<string,'retro'>;// truetypeaa=Retroextendsstring ? true : false;// truetypebb=Retroextendsobject ? true : false;In my case, today I had to special case a type conditional to avoid having my tag types be treated as objects. With the new behavior exhibited by the builtin implementation here, I wouldn't have had to. So, I'd consider this an improvement over the DIY solution. But probably worth noting for anyone trying to convert from DIY to builtin. |
sandersn commented May 24, 2022
This experiment is pretty old, so I'm going to close it to reduce the number of open PRs. |
shicks commented May 26, 2022
Would the authors consider revisiting this work? There seem to be quite a few people interested in the feature (see the recent comments on #33038), though this approach seems to be the better approach. |
RyanCavanaugh commented May 27, 2022
This is effectively an implementation sketch for #202. We'd prefer people interact with the suggestion issues than the PRs, since the implementation of a feature is mechanical once the design has been worked out, and nominal types are still very much under discussion |
Consider this a competitor to (or at least consideration for) #33038.
This makes explicit the current patterns of structural branding and reserves their functionality with special syntax. The newly introduced syntax is the new keyword type operator
tag T, whereTcan be any type. It is used like so:This PR also now adds a global
type Tag<T extends string> = tag{[K in T]: void};for convenience, which means instead of the above, you can write:which gives a simple way to get a nice string-based pseudo-unique tag.
The operand type does not contribute to the visible structure of the type in any way (a
tagstill looks likeunknownwhen queried), howevertagtypes are related with one another based on their argument type to ensure tag compatibility.Compared with nominal tags, this has some upsides: