- Notifications
You must be signed in to change notification settings - Fork 13.2k
Nominal unique type brands#33038
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
Nominal unique type brands #33038
Uh oh!
There was an error while loading. Please reload this page.
Conversation
weswigham commented Aug 23, 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.
fatcerberus commented Aug 23, 2019
I don't understand this part. If these are meant to replace branded types, then: typeUString= unique string;typeBString=string&{__brand: true};declareletustr: UString;declareletbstr: BString;letstr1: string=bstr;// ok because branded string is a subtype of string (this is generally desirable).letstr2: string=ustr;// could be ok because unique string is also a string.letnum: number=ustr;// never ok, should be error because unique string is NOT a number!But if we just had |
weswigham commented Aug 23, 2019
It's useful because you can then intersect it with something. Which is all |
fatcerberus commented Aug 23, 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.
Yeah, I need to read more closely. I just noticed the It might be represented as an intersection under the hood, but that strikes me as unnecessarily exposing implementation details. |
weswigham commented Aug 23, 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.
|
cevek commented Aug 23, 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.
Is this proposal allows to assign literals to variable/param which has branded type? typeUserId= unique stringfunctionfoo(param: UserId){}foo("foo")// okvarx: UserId="foo";// oklets="str";vary: UserId=s;// not okfoo(s)// not ok |
goodmind commented Aug 23, 2019
How would you make nominal classes with this? |
weswigham commented Aug 23, 2019
Not easily. You'd need to mix a distinct nominal brand into every class declaration, like via a property you don't use of type I'd avoid it, if possible, tbh. Nominal classes sound like a pain :P |
weswigham commented Aug 23, 2019
Branded types can only be "made" either via cast or typeguard, as I said in the OP, so no. This is because the typesystem doesn't know what invariants a given brand is meant to maintain, and can't implicitly know if some literal satisfies them. |
jack-williams commented Aug 23, 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 something like the following ever be meaningful? Probably not right.. interfaceParentextendsuniqueunknown{}interfaceChildAextends(Parent&uniqueunknown){}interfaceChildBextends(Parent&uniqueunknown){} |
AnyhowStep commented Aug 23, 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 is probably obvious to everyone but For example, //Can be replaced with `unique number`typeLessThan256=number&{__rangeLt : 256}//Cannot be replaced with `unique number`typeLessThan<Nextendsnumber>=number&{__rangeLt : N}More complicated example here, There are a few reasons why I'm generally against the idea of Cross-library interopLibrary A may have Both types will be considered different, even though they have the same name and declaration. If libraries start using this So, one starts thinking that a no-op casting function would be safer, functionlibARadianToLibBRadian(rad : libA.Radian) : libB.Radian{returnradaslibB.Radian;}This is safer because you won't accidentally convert But if you have Cross-version interopIt's happened to me a bunch where I've had the same package, but at different versions, within a single project. So, v1.0.0's Now you need a casting function... Even though it's the same package. With brands, if two libraries use the same brands, even if they're different types, they'll still be assignable to each other. (As long as they don't use Library A may have Even though they're different types, they're assignable to each other. No casting needed. As an aside, I vaguely remember something from many, many years ago. I can't find it through Google anymore, though. There was discussion about adding syntax to C++ to make typedefdouble Radian;And this was rejected outright because of the issues I listed above. Two libraries with their own |
fatcerberus commented Aug 23, 2019
Re: nominal classes - classes are already nominal if they contain any private members. Just throwing that out there. 😃 |
be5invis commented Aug 24, 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 we can finally have things like this? @weswigham // Low-end refinement type :)typeNonEmptyArray<A>= unique ReadonlyArray<A>functionisNonEmpty(a: ReadonlyArray<A>): a is NonEmptyArray<A>{returna.length>0}// INTEGERS (sort of)typeinteger= unique numberfunctionisInteger(a: number){returna===a|0} |
AnyhowStep commented Aug 24, 2019
@fatcerberus It's also why I avoid classes entirely and avoid private members if I do have them =P |
AnyhowStep commented Aug 25, 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 commented Aug 26, 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 cross version compatibility really is an issue then an alternate solution would be to make naming explicit: Let the type typeNormalizedPathOne=string& unique "NormalizedPath";where an unlabelled typeNormalizedPathTwo=string& unique // some label that we don't care about that is auto-generated.so while two declarations of FWIW I have no real preference---for me the big win of this feature is being able reduce empty intersections more aggressively.
IMO, for all brand oblivious operations a |
resynth1943 commented Aug 27, 2019
I've implemented Opaque types like so: typeOpaque<V>=V&{readonly__opq__: unique symbol};typeAccountNumber=Opaque<number>;typeAccountBalance=Opaque<number>;functioncreateAccountNumber(): AccountNumber{return2asAccountNumber;}functiongetMoneyForAccount(accountNumber: AccountNumber): AccountBalance{return4asAccountBalance;}getMoneyForAccount(100);// -> error |
AnyhowStep commented Aug 27, 2019
Your version breaks given the following, typeOpaque<V>=V&{readonly__opq__: unique symbol};typeNormalizedPath=Opaque<string>;typeAbsolutePath=Opaque<string>;typeNormalizedAbsolutePath=NormalizedPath&AbsolutePath;declarefunctionisNormalizedPath(x: string): x is NormalizedPath;declarefunctionisAbsolutePath(x: string): x is AbsolutePath;declarefunctionconsumeNormalizedAbsolutePath(x: NormalizedAbsolutePath): void;constp="/a/b/c";consumeNormalizedAbsolutePath(p);//Errorif(isNormalizedPath(p)){consumeNormalizedAbsolutePath(p);//Expected Error, Actual OKif(isAbsolutePath(p)){consumeNormalizedAbsolutePath(p);//OK}}Contrast with, |
resynth1943 commented Aug 31, 2019
@AnyhowStep I know, but that's how I'm currently creating opaque types. I hope this Pull Request will incorporate this into the language, and make it even better than my implementation. |
mohsen1 commented Sep 3, 2019
Nominal types are pretty useful. The example I often use is the APIs that take latitude/longitude and bugs that are result of mixing up latitude with longitude which are both numbers. By making those unique types we can avoid that class of bugs. However, unique types can cause so much pain when you have to keep importing those types to simply use an API. So I'm hoping that at least primitive types are assignable to unique primitives where I can still call my functions like this: // lib.tsexporttypeLat= unique number;exporttypeLng= unique number;exportfunctiondistance(lat: Lat,lng: Lng): number;// usage.tsimport{distance}from'lib.ts';distance(1234,5678);// no need to asset typesAs @AnyhowStep mentioned cross-lib and cross-version conflicting unique types can also be a source of pain. Can we limit uniqueness scope somehow? Would that be a viable solution? |
weswigham commented Sep 6, 2019
#33290 is now open as well, so we can have a real conversation on what the non-nominal explicit tag would look like, and if we'd prefer it. |
weswigham commented Sep 7, 2019
BTW, this is now a dueling features type situation (though I've authored both) - we won't accept both a #33290 style brand and this PR's style brand, only one of the two (and we're leaning towards #33290 on initial discussion). We'll get to debating it within the team hopefully during our next design meeting (next friday), but y'all should express ideas, preferences, and reasoning therefore within both PRs. |
weswigham commented Sep 9, 2019
Only repo contributors have permission to request that @typescript-bot pack this |
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 13968b0. 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 |
xiaoxiangmoe 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.
typeinteger= unique numberfunctionisInteger(a: number): a is integer{returna===(a|0)}functionInterger(a:number){if(!isInteger(a))thrownewError("not an integer");returna;} |
dead-claudia commented May 28, 2020
Is there a status update on this PR? |
dead-claudia commented Oct 14, 2020
@weswigham Couple questions:
|
weswigham commented Oct 22, 2020
Oh, wow, it's been that long. Uh. Last time I presented it to the team, the response was somewhat lukewarm - there's no real excitement or drive for it right now. So I guess what we're looking to see is overwhelming demand? |
kachkaev commented Oct 22, 2020 • 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 sure how the demand is properly measured, but this PR is in top 5 upvoted open PRs at the moment. Just saying 😁 |
dead-claudia commented Nov 2, 2020
@weswigham Thanks for the quick explanation! I see why both are necessary, now. Also, I definitely recommend looking into @kachkaev's comment above. |
alexweej commented Jan 14, 2021
Just used branding to very quickly identify the inconsistencies in a code base that was using string for 3 different logical dynamically valued types. Consider this my +1... |
ProdigySim commented Nov 30, 2021 • 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.
Just a bit of data: The last codebase I was in changed from using We wanted to move to a code generation system for our web API types. We had backend code that used nominal types ("tinytypes") in Scala which was the source of our branding. Using unique-symbol based types had a couple of drawbacks here:
We ended up converting all of our nominal types to string-based tag types overnight to support this project. They solved all of these problems:
tl;dr string-based tag types seem a little more flexible with implementation details and are still good enough. They still have the hard problem of naming things, but they are fixable when there are name conflicts. We can also put out recommendations for tag patterns in shared libraries. |
sandersn commented May 24, 2022
This experiment is pretty old, so I'm going to close it to reduce the number of open PRs. |
negue commented May 24, 2022
Will there be any type of these I needed something like that in a project where I had to juggle like a couple of simple "number" IDs and here something like unique number type per ID would've helped to save some time debugging :D |
shicks commented May 25, 2022
Is there a particular reason this experiment stalled out? |
evelant commented May 26, 2022 • 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.
@sandersn This is the 9th most voted on PR in the entire history of the repo, I think that's a clear indicator that a lot of people want it and it's probably worth pushing forward. Would you please reopen it? IMO this is a very valuable feature. Nominal types can greatly increase type safety in any case where a value has a specific semantic meaning and thus should not be compatible with any similarly shaped type (or primitive) as is the default. Currently that's a difficult problem to solve with typescript. I would imagine nominal types could also greatly improve compiler performance by skipping structural comparisons. IMO this should not be summarily closed simply because it hasn't had attention recently. @weswigham since you authored this, do you think it is worth continuing the work you started or should this be closed in favor of a new discussion/implementation? |
weswigham commented May 26, 2022
I mean, personally, I think the structural version of this PR, the |
evelant commented May 26, 2022
@weswigham I'm not familiar with the PR you're referring to, would you link to it please? |
weswigham commented May 26, 2022
evelant commented May 26, 2022
Thanks, I agree that PR seems like it is a better approach. Unfortunately it looks like that one got summarily closed as well. Given that there seems to be a lot of interest in support for some form of nominal typing perhaps it should be reopened? |
sandersn commented May 27, 2022
From the last comment on the other PR:
|
ruojianll commented Jul 26, 2024
@weswigham Hey great man, how was this going? |

Fixes#202
Fixes#4895
We've talked about this on and off for the last three years, and it was a major reason we chose to use
unique symbolfor the individual-symbol-type, since we wanted to reuse the operator for a nominal tag later. What this PR allows:unique T(whereTis any type) is allowed in any position a type is allowed, and nominally tagsTwith a marker that makes onlyT's that have come from that location be assignable to the resulting type.This is done by adding a new
NominalBrandtype flag, which is a type with no structure which is unique to each symbol it is manufactured from. This is then mixed into the argument type tounique typevia intersection, which is what produces all useful relationships. (The brand can have an alias if it is directly constructed viatype MyBrand = unique unknown)This does so much with so little - this reduces the jankiness written into types to enable nominalness with
unique symbols orenums, while adding zero new assignability rules.So, why bring this up now? I was thinking about how "brands" work today, with something like
type MyBrand<T> = T &{[myuniquesym]: void}whereTcould then become a literal type like"a". We've wanted, for awhile, to be able to more eagerly reduce an intersection of an object literal and a primitive tonever(to make subtype reduction and intersection reduction produce less jank and recognize more types as mutually exclusive), but these "brand" patterns keep stopping us. (Heck, we use em internally.) Well, if we ever want to change object types to actually mean object, then we're going to need to provide an alternative for the brand pattern, and ideally that alternative needs to be available for awhile. So looking on the horizon to breaks we could take into 4.0 in 9 months, this simplification of branding would be up there, provided we've had the migration path available for awhile. So I'm trying to get the conversation started on this before we're too close to that deadline to plan something like that. Plus #202 is up there on our list of all-time most requested issues, and while we've always been open to it, we've never put forward a proposal of our own - well, here one is.On
unique symbolunique symbol's current behavior takes priority for syntactically exactlyunique symbol, however if a nominal subclass of symbols is actually desired, one can writeunique (symbol)to get a nominally brandedsymboltype instead (orsymbol & unique unknown- it's exactly the same thing). The way aunique symbolbehaves is like a symbol that is locked to a specific declaration, and has special abilities when it comes to narrowing and control flow because of that. Nominally branded types are more flexible in their usage but do not have the strong control-flow linkage with a host statement thatunique symbols do (in fact, they don't necessarily assume a value exists at all), so there is very much reason for them to coexist. They're similar enough, that I'm pretty comfortable sharing syntax between the two.Alternative considerations
While I've used the
uniquetype operator here, like we've oft spoken of, on implementation, it's become plain to me that I don't need to specify an argument tounique. We could just exposeuniqueas a unique type factory on it's own, and dispense with the indirection. The "uniqueness" we apply internally isn't actually tied to the input type argument through anything more than an intersection type, so simply shorteningunique unknowntouniqueand reserving the argument form for justunique symbols may be preferable. All the same patterns would be possible, one would just need to writestring & uniqueinstead ofunique string, thus dispensing with the sugar. It depends on the perceived complexity, I think. However, despite being exactly the same,string & uniqueis somehow uglier and harder to look at thanunique string, which is why I've kept it around for now. It's probably worth discussing, though.What this draft would still need to be completed:
One or more unique brands is missing from type Awith related information pointing at the brand location, rather than the current error involvingunique unknown)unique unknownbrands)unique (symbol)declaration emit (to ensure it's not rewritten asunique symbol)keyof <unique brand type>should benever(as it is now), since the brand is top-ish (and contains no structure information itself), or if it should be preserved as an abstractkeyof <unique brand>type, so that brand can apply keys-of-branded-types constraints in constraint positions