In most languages, doing what this article describes is quite straightforward: you would just define a new type (/ struct / class) called ‘Hash’, which functions can take or return. The language automatically treats this as a completely new type. This is called ‘nominal typing’: type equality is based on the name of the type.
The complication with TypeScript is that it doesn’t have nominal typing. Instead, it has ‘structural typing’: type equality is based on what the type contains. So you could define a new type ‘Hash’ as a string, but ‘Hash’ would just be a synonym — it’s still considered interchangeable with strings. This technique of ‘branded types’ is simply a way to simulate nominal typing in a structural context.
> In most languages, doing what this article describes is quite straightforward
Well, no. In most languages you wind up making a typed wrapper object/class that holds the primitive. This works fine, you can just do that in TypeScript too.
The point of branded types is that you're not introducing a wrapper class and there is no trace of this brand at runtime.
What you say is true, but after years of working with TypeScript (and about 15 years of Java before that) I'd say that from a purely practical perspective the structural typing approach is much more productive.
I still have PTSD from the number of times I had to do big, painful refactorings in Java simply because of the way strong typing with nominal types works. I still shudder to think of Jersey 1.x to 2.x migrations from many years ago (and that was a PITA for many reasons beyond just nominal typing, but it could have been a lot easier with structural types).
I love branded types (and what I think of their close cousin of string template literal types in TS) because they make code safer and much more self-documenting with minimal effort and 0 runtime overhead.
Typescript has good reasons to default to Structural Typing as untagged union type are one of the most used types in typing js code and Nominal Typing does not really have a good equivalent for them.
It took me so long to fully appreciate TypeScript's design decision for doing structural typing vs. nominal typing. In all scenarios, including the "issue" highlighted in this article there is no reason for wanting nominal typing.
And for `hash.toUpperCase()`, it's a valid program. TypeScript is not designed to stop you from using string prototype methods on... strings!
It's more pronounced in object types that some library authors don't want you to pass an object that conforms to the required shape and insist on passing result of some function they provide. e.g. `mylib.foo(mylib.createFooOptions({...})`. None of that is necessary IMO
> And for `hash.toUpperCase()`, it's a valid program.
In a sense, but it's not the program we wanted to write, and types can be a useful way of moving that kind of information around within a program during development.
> TypeScript is not designed to stop you from using string prototype methods on... strings!
No, but it is designed to let me design my types to stop myself from accidentally using string prototype methods on data to which they don't actually apply, even when that data happens to be represented as... strings.
Template literal types solve ordering for a very specific type of parameter-order problems which happens to include the (explicitly identified as an example) terrible hash function that just prepends "hashed_".
But what about when you have an actual hash function that can't be reasonably represented by a template literal type? What about when the strings are two IDs that are different semantically but identical in structure? What about wanting to distinguish feet from inches from meters?
Don't get me wrong, I like structural typing, but there are all kinds of reasons to prefer nominal in certain cases. One reason why I like TypeScript is that you can use tricks like the one in TFA to switch back and forth between them as needed!
This example is also an odd choice because... it's not the right way to do it. If you're super concerned about people misusing hashes, using string as the type is a WTF in itself. Strings are unstructured data, the widest possible value type, essentially "any" for values that can be represented. Hashes aren't even strings anyway, they're numbers that can be represented as a string in base-whatever. Of course any such abstraction leaks when prodded. A hash isn't actually a special case of string. You shouldn't inherit from string.
If you really need the branded type, in that you're inheriting from a base type that does more things than your child type.... you straight up should not inherit from that type, you've made the wrong abstraction. Wrap an instance of that type and write a new interface that actually makes sense.
I also don't really get what this branded type adds beyond the typical way of doing it i.e. what it does under the hood, type Hash = string & { tag: "hash" }. There's now an additional generic involved (for funnier error messages I guess) and there are issues that make it less robust than how it sells itself. Mainly that a Branded<string, "hash"> inherits from a wider type than itself and can still be treated as a string, uppercased and zalgo texted at will, so there's no real type safety there beyond the type itself, which protects little against the kind of developer who would modify a string called "hash" in the first place.
How do you do this with template literal types? Does that mean you changed the string that gets passed at runtime?
The nice thing about branding (or the "flavored" variant which is weaker but more convenient) is that it's just a type check and nothing changes at runtime.
> In this case where the wrong order of parameters was the issue, you can solve it with Template Literal Types
You can solve the issue in this particular example because the "hashing" function happens to just append a prefix to the input. There is a lot of data that isn't shaped in that manner but would be useful to differentiate nonetheless.
> And for `hash.toUpperCase()`, it's a valid program.
It's odd to try and argue that doing uppercasing a hash is okay because the hash happens to be represented as a string internally, and strings happen to have such methods on them. Yes, it's technically a valid program, but it's absolutely not correct to manipulate hashes like that. It's even just odd to point out that Typescript includes string manipulation methods on strings. The whole point of branding like this is to treat the branded type as distinct from the primitive type, exactly to avoid this correctness issue.
One tricky scenario I stumbled on is `.toString()`.
Everything has a `.toString()` but some objects A have `.toString(arg1, arg2, arg3)`. But replacing A with something that does not have toString with arguments still type checks, yet will probably result in serious error.
> In all scenarios [...] there is no reason for wanting nominal typing.
Hard disagree.
It's very useful to e.g. make a `PasswordResetToken` be different from a `CsrfToken`.
Prepending a template literal changes the underlying value and you can no longer do stuff like `Buffer.from(token, 'base64')`. It's just a poor-man's version of branding with all the disadvantages and none of the advantages.
You can still `hash.toUpperCase()` a branded type. It just stops being branded (as it should) just like `toUpperCase` with `hashed_` prepended would stop working... except `toLowerCase()` would completely pass your template literal check while messing with the uppercase characters in the token (thus it should no longer be a token, i.e. your program is now wrong).
Additionally branded types can have multiple brands[0] that will work as you expect.
So a user id from your DB can be a `UserId`, a `ModeratorId`, an `AdminId` and a plain string (when actually sending it to a raw DB method) as needed.
Try doing this (playground in [1]) with template literals:
type UserId = Tagged<string, 'UserId'>
type ModeratorId = Tagged<UserId, 'ModeratorId'> // notice we composed with UserId here
type AdminId = Tagged<UserId, 'AdminId'> // and here
const banUser = (banned: UserId, banner: AdminId) => {
console.log(`${banner} just banned ${banned.toUpperCase()}`)
}
const notifyUser = (banned: UserId, notifier: ModeratorId) => {
console.log(`${notifier} just notified ${banned.toUpperCase()}`) // notice toUpperCase here
}
const banUserAndNotify = (banned: UserId, banner: ModeratorId & AdminId) => {
banUser(banned, banner)
notifyUser(banned, banner)
}
const getUserId = () =>
`${Math.random().toString(16)}` as UserId
const getModeratorId = () =>
// moderators are also users!
// but we didn't need to tell it explicitly here with `as UserId & ModeratorId` (we could have though)
`${Math.random().toString(16)}` as ModeratorId
const getAdminId = () =>
// just like admins are also users
`${Math.random().toString(16)}` as AdminId
const getModeratorAndAdminId = () =>
// this is user is BOTH moderator AND admin (and a regular user, of course)
// note here we did use the `&` type intersection
`${Math.random().toString(16)}` as ModeratorId & AdminId
banUser(getUserId(), getAdminId())
banUserAndNotify(getUserId(), getAdminId()) // this fails
banUserAndNotify(getUserId(), getModeratorId()) // this fails too
banUserAndNotify(getUserId(), getModeratorAndAdminId()) // but this works
banUser(getAdminId(), getAdminId()) // you can even ban admins, because they're also users
console.log(getAdminId().toUpperCase()) // this also works
getAdminId().toUpperCase() satisfies string // because of this
banUser(getUserId(), getAdminId().toUpperCase()) // but this fails (as it should)
getAdminId().toUpperCase() satisfies AdminId // because this also fails
You can also do stuff like:
const superBan = <T extends UserId>(banned: Exclude<T, AdminId>, banner: AdminId) => {
console.log(`${banner} just super-banned ${banned.toUpperCase()}`)
}
superBan(getUserId(), getAdminId()) // this works
superBan(getModeratorId(), getAdminId()) // this works too
superBan(getAdminId(), getAdminId()) // you cannot super-ban admins, even though they're also users!
Pascal worked that way all the time, and it was hated. You could have "inch" and "meter" version of integer, and they were not interchangeable. This was sometimes called "strong typing"
It's interesting that in Rust, "type" does not work that way. I kind of expected that it would. But no, "type" in Rust is just an alternate name, like "typedef" in C.
Both approaches are useful at different times. For example you wouldn't want to accidentally multiple a meter by a centimeter but you may want to provide std::io::Result<T> which is equivalent to Result<T, std::io::Error> but just a bit nicer to type.
For example in Rust you can do:
type Foo = Bar;
Which is just an alias, interchangeable with Bar.
Or you can do:
struct Foo(Bar);
Which is a completely new type that just so happens to contain a Bar.
It is a form of strong typing because integer could be the length of your toe nail, a temperature or the seconds since the unix epoch.
Sometimes you really want to make sure someone is not going to introduce billion dollar bugs, by making the type different from the underlying representation. In Haskell that would be sth like
newtype Temperature = Int
At other times, you just want to document in stead of forcing semantics. A contrived example:
type AgeMin = Int
type AgeMax = Int
isAdmissible :: AgeMin -> AgeMax -> Bool
isAdmissible :: Int -> Int -> Bool // less clear
As someone who values a tight domain model (a la DDD) and primarily writes TypeScript, I've considered introducing branded types many times, and always decline. Instead, we just opt for "aliases," especially of primatives (`type NonEmptyString = string`), and live with the consequences.
The main consequence is that we need an extra level of vigilance and discipline in PR reviews, or else implicit trust in one another. With a small team, this isn't difficult to maintain, even if it means that typing isn't 100% perfect in our codebase.
I've seen two implementations of branded types. One of them exploits a quirk with `never` and seems like a dirty hack that might no longer work in a future TS release. The other implementation is detailed in this article, and requires the addition of unique field value to objects. In my opinion, this pollutes your model in the same way that a TS tagged union does, and it's not worth the trade-off.
When TypeScript natively supports discriminated unions and (optional!) nominal typing, I will be overjoyed.
> The other implementation is detailed in this article, and requires the addition of unique field value to objects.
That's not quite what ends up happening in this article though. The actual objects themselves are left unchanged (no new fields added), but you're telling the compiler that the value is actually an intersection type with that unique field. There a load-bearing `as Hash` in the return statement of `generateHash` in the article's example that makes it work without introducing runtime overhead.
I definitely agree about native support for discriminated unions / nominal typing though, that would be fantastic.
I like the brevity of this blog post, but it's work noting that this mostly feels like a workarounds for Typescript not supporting any form of nominal typing or "opaque type" like in Flow.
Flow has actual support for this with opaque types. You just use the opaque keyword in front of a type alias ˋopaque type Hash = string` and then that type can only be constructed in the same file where it is defined. Typescript could introduce a similar feature
Note that the prefix was never intended to be looked at as the real problem. That's not a hash function, that's an example hash function because TFA couldn't be bothered to implement a proper one. They're not actually trying to solve the prefix problem.
Isn't there a risk with this approach that you may receive input with a repeated prefix when there's a variable of type `string` and the prefix is prepended to satisfy the type checker without checking if the prefix already exists?
I had the displeasure of working with a Flow codebase that typed every string and int uniquely like this. I could see the benefit if you’re working on something mission critical where correctness is paramount, but in your average web app I think it just creates a lot of friction and busy work with no real benefit.
That's nice but it seems you're in search of a nominal type system within a structurally typed language. I'd posit that it's usually much better to step and try to approach the problem from the way the language lends itself to be solved rather than trying to hack it to fit your expectations
Do you mind elaborating on why this approach would be bad in general? It avoids the overhead of creating new classes and wrapping your objects when all you care about is the type-safety that the class would provide.
How would you "approach the problem from the way the language lends itself to be solved"?
I like to make the 'brand' property optional so that it doesn't actually have to be there at runtime, but the TypeScript type-checker will still catch mismatches
type USDollars = number & { currency?: "USD" }
(or something like that; My TypeScript-fu may be slightly rusty at the moment.)
Of course a `number` won't have a currency property, but if it did, its value would be "USD"!
I've found that TypeScript's structural typing system fits my brain really well, because most of the time it is what I want, and when I really want to discriminate based on something more abstract, I can use the above trick[1], and voila, effectively nominal types!
[1] With or without the tag being there at runtime, depending on the situation, and actually I do have a lot of interface definitions that start with "classRef: " + some URI identifying the concept represented by this type. It's a URI because I like to think of everything as if I were describing it with RDF, you see.
Making it optional doesn't work, the brand property needs to be required.
Doesn't mean that you actually have to define the property at runtime, you'd usually cast the number to USDollars where relevant
One nuance missing from the article is that since branded / tagged types extend from the base type, callers can still see and use string methods, which may not be what you want.
Equality can be problematic too. Imagine an Extension type, one could compare it with ".mp4" or "mp4", which one is correct?
Opaque types (that extend from `unknown` instead of T) work around these problems by forcing users through selector functions.
Also, since all examples of branded / nominal types in TypeScript use `as` (I assume to get around the fact that the object you're returning isn't actually of the shape you're saying it is...), you should read up on the pitfalls of it:
Interesting way of elevating bug issue to compile time. I'll definitely try to apply it to my TypeScript Front-End.
I use the newtype pattern a lot in Rust. I try to avoid passing strings around. Extracting information over and over is cumbersome. Ensuring behavior is the same is big-ridden.
An example: is a string in the email address format? Parse it to an Email struct which is just a wrapper over String.
On top of that we then assign behavior to the type, like case insensitive equality. Our business requires [email protected] to be the same as [email protected]. This way we avoid the developer having to remember to do the checks manually every time. It's just baked into the equality of the type.
But in Rust using
type Email = String;
just creates an alias. You really have to do something like
struct Email(String)
Also, I know the only way to test an email address is to send an email and see if the user clicks a link. I reply should introduce a trait and a ValidatedEmail and a NotValidatedEmail.
I don't understand why users of branded types use strings as brands. Also using a utility type makes unreadable the TypeScript errors related to this type.
If I had to use branded types, I personally would prefer a different approach:
declare const USER_BRAND: unique symbol
type User = { name: string, [USER_BRAND]: undefined }
This also allows subtyping:
declare const PERSON_BRAND: unique symbol
type Person = { name: string, age: number, [USER_BRAND]: undefined, [PERSON_BRAND]: undefined }
Has something changed in TypeScript? There have been a few of these blog posts about branded types lately, but afaik, it's been possible to do this for years.
One of the projects I work on is crying out for better support for nominal typing. It involves manipulating music notation, and there are so many things passed around that are all strings, but semantically different: a note ("A"), a pitched note ("A4"), a key ("A minor"), etc etc etc. Life would definitely be easier if I could just quickly and conveniently declare the types to be distinct.
I do use the branded types thing, but it's a bit clunky and sometimes gets in the way. And the error messages you get aren't as clear as you would like.
Giving types names is sometimes useful, yes. To the extent that languages often have that as the only thing and maybe have somewhat second class anonymous types without names.
It's called "nominal typing", because the types have names. I don't know why it's called "branded" instead here. There's probably a reason for the [syntax choice].
It's called branded because the article is talking about branded types a way to enable nominal typing inside the structural typing system of Typescript.
You're going to hate that you did that when you want to write a function that prints out any hash, surely?
We once did a similar thing in C++, creating types for values in different units. Complete nightmare, everyone hated it. We went back to using 'double' for all floating point values.
I ran the branded type example listed in the blog through bun and it ran without issuing a warning or error for the "This won't compile!" code. Is there any way to get bun to be strict for TypeScript errors?
[+] [-] bradrn|1 year ago|reply
The complication with TypeScript is that it doesn’t have nominal typing. Instead, it has ‘structural typing’: type equality is based on what the type contains. So you could define a new type ‘Hash’ as a string, but ‘Hash’ would just be a synonym — it’s still considered interchangeable with strings. This technique of ‘branded types’ is simply a way to simulate nominal typing in a structural context.
[+] [-] beeboobaa3|1 year ago|reply
Well, no. In most languages you wind up making a typed wrapper object/class that holds the primitive. This works fine, you can just do that in TypeScript too.
The point of branded types is that you're not introducing a wrapper class and there is no trace of this brand at runtime.
[+] [-] hn_throwaway_99|1 year ago|reply
I still have PTSD from the number of times I had to do big, painful refactorings in Java simply because of the way strong typing with nominal types works. I still shudder to think of Jersey 1.x to 2.x migrations from many years ago (and that was a PITA for many reasons beyond just nominal typing, but it could have been a lot easier with structural types).
I love branded types (and what I think of their close cousin of string template literal types in TS) because they make code safer and much more self-documenting with minimal effort and 0 runtime overhead.
[+] [-] afiori|1 year ago|reply
[+] [-] jacobsimon|1 year ago|reply
class Hash extends String {}
https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
[+] [-] eru|1 year ago|reply
Eg in Rust or Haskell you can distinguish `Option<Option<bool>>`, but not in these languages. I guess Python and Typescript are examples of these?
[+] [-] msoad|1 year ago|reply
In this case where the wrong order of parameters was the issue, you can solve it with [Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-lite...). See [1].
And for `hash.toUpperCase()`, it's a valid program. TypeScript is not designed to stop you from using string prototype methods on... strings!
It's more pronounced in object types that some library authors don't want you to pass an object that conforms to the required shape and insist on passing result of some function they provide. e.g. `mylib.foo(mylib.createFooOptions({...})`. None of that is necessary IMO
[1] https://www.typescriptlang.org/play/?#code/MYewdgzgLgBA5gUzA...
[+] [-] dllthomas|1 year ago|reply
In a sense, but it's not the program we wanted to write, and types can be a useful way of moving that kind of information around within a program during development.
> TypeScript is not designed to stop you from using string prototype methods on... strings!
No, but it is designed to let me design my types to stop myself from accidentally using string prototype methods on data to which they don't actually apply, even when that data happens to be represented as... strings.
[+] [-] lolinder|1 year ago|reply
But what about when you have an actual hash function that can't be reasonably represented by a template literal type? What about when the strings are two IDs that are different semantically but identical in structure? What about wanting to distinguish feet from inches from meters?
Don't get me wrong, I like structural typing, but there are all kinds of reasons to prefer nominal in certain cases. One reason why I like TypeScript is that you can use tricks like the one in TFA to switch back and forth between them as needed!
[+] [-] lIIllIIllIIllII|1 year ago|reply
If you really need the branded type, in that you're inheriting from a base type that does more things than your child type.... you straight up should not inherit from that type, you've made the wrong abstraction. Wrap an instance of that type and write a new interface that actually makes sense.
I also don't really get what this branded type adds beyond the typical way of doing it i.e. what it does under the hood, type Hash = string & { tag: "hash" }. There's now an additional generic involved (for funnier error messages I guess) and there are issues that make it less robust than how it sells itself. Mainly that a Branded<string, "hash"> inherits from a wider type than itself and can still be treated as a string, uppercased and zalgo texted at will, so there's no real type safety there beyond the type itself, which protects little against the kind of developer who would modify a string called "hash" in the first place.
[+] [-] stiiv|1 year ago|reply
This isn't valuable to you? How do you get this without nominal typing, especially of primatives?
[+] [-] skybrian|1 year ago|reply
The nice thing about branding (or the "flavored" variant which is weaker but more convenient) is that it's just a type check and nothing changes at runtime.
[+] [-] mattstir|1 year ago|reply
You can solve the issue in this particular example because the "hashing" function happens to just append a prefix to the input. There is a lot of data that isn't shaped in that manner but would be useful to differentiate nonetheless.
> And for `hash.toUpperCase()`, it's a valid program.
It's odd to try and argue that doing uppercasing a hash is okay because the hash happens to be represented as a string internally, and strings happen to have such methods on them. Yes, it's technically a valid program, but it's absolutely not correct to manipulate hashes like that. It's even just odd to point out that Typescript includes string manipulation methods on strings. The whole point of branding like this is to treat the branded type as distinct from the primitive type, exactly to avoid this correctness issue.
[+] [-] unknown|1 year ago|reply
[deleted]
[+] [-] vjerancrnjak|1 year ago|reply
Everything has a `.toString()` but some objects A have `.toString(arg1, arg2, arg3)`. But replacing A with something that does not have toString with arguments still type checks, yet will probably result in serious error.
[+] [-] kaoD|1 year ago|reply
Hard disagree.
It's very useful to e.g. make a `PasswordResetToken` be different from a `CsrfToken`.
Prepending a template literal changes the underlying value and you can no longer do stuff like `Buffer.from(token, 'base64')`. It's just a poor-man's version of branding with all the disadvantages and none of the advantages.
You can still `hash.toUpperCase()` a branded type. It just stops being branded (as it should) just like `toUpperCase` with `hashed_` prepended would stop working... except `toLowerCase()` would completely pass your template literal check while messing with the uppercase characters in the token (thus it should no longer be a token, i.e. your program is now wrong).
Additionally branded types can have multiple brands[0] that will work as you expect.
So a user id from your DB can be a `UserId`, a `ModeratorId`, an `AdminId` and a plain string (when actually sending it to a raw DB method) as needed.
Try doing this (playground in [1]) with template literals:
You can also do stuff like: [0] https://github.com/sindresorhus/type-fest/blob/main/source/o...[1] https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEYD2...
[+] [-] Animats|1 year ago|reply
It's interesting that in Rust, "type" does not work that way. I kind of expected that it would. But no, "type" in Rust is just an alternate name, like "typedef" in C.
[+] [-] kevincox|1 year ago|reply
For example in Rust you can do:
Which is just an alias, interchangeable with Bar.Or you can do:
Which is a completely new type that just so happens to contain a Bar.[+] [-] earleybird|1 year ago|reply
https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
[+] [-] exceptione|1 year ago|reply
Sometimes you really want to make sure someone is not going to introduce billion dollar bugs, by making the type different from the underlying representation. In Haskell that would be sth like
At other times, you just want to document in stead of forcing semantics. A contrived example:[+] [-] stiiv|1 year ago|reply
The main consequence is that we need an extra level of vigilance and discipline in PR reviews, or else implicit trust in one another. With a small team, this isn't difficult to maintain, even if it means that typing isn't 100% perfect in our codebase.
I've seen two implementations of branded types. One of them exploits a quirk with `never` and seems like a dirty hack that might no longer work in a future TS release. The other implementation is detailed in this article, and requires the addition of unique field value to objects. In my opinion, this pollutes your model in the same way that a TS tagged union does, and it's not worth the trade-off.
When TypeScript natively supports discriminated unions and (optional!) nominal typing, I will be overjoyed.
[+] [-] anamexis|1 year ago|reply
You can already do this:
And this will compile: Whereas this wont:[+] [-] mattstir|1 year ago|reply
That's not quite what ends up happening in this article though. The actual objects themselves are left unchanged (no new fields added), but you're telling the compiler that the value is actually an intersection type with that unique field. There a load-bearing `as Hash` in the return statement of `generateHash` in the article's example that makes it work without introducing runtime overhead.
I definitely agree about native support for discriminated unions / nominal typing though, that would be fantastic.
[+] [-] sleazy_b|1 year ago|reply
[+] [-] jakubmazanec|1 year ago|reply
[+] [-] mpawelski|1 year ago|reply
[+] [-] freeney|1 year ago|reply
[+] [-] chromakode|1 year ago|reply
https://www.kravchyk.com/adding-type-safety-to-object-ids-ty...
[+] [-] lolinder|1 year ago|reply
[+] [-] comagoosie|1 year ago|reply
[+] [-] dyeje|1 year ago|reply
[+] [-] culi|1 year ago|reply
[+] [-] mattstir|1 year ago|reply
How would you "approach the problem from the way the language lends itself to be solved"?
[+] [-] kookamamie|1 year ago|reply
[+] [-] jweir|1 year ago|reply
https://package.elm-lang.org/packages/ianmackenzie/elm-units...
Very nice to prevent conversions between incompatible units, but without the over head of lots of type variants.
https://thoughtbot.com/blog/modeling-currency-in-elm-using-p...
[+] [-] TOGoS|1 year ago|reply
Of course a `number` won't have a currency property, but if it did, its value would be "USD"!
I've found that TypeScript's structural typing system fits my brain really well, because most of the time it is what I want, and when I really want to discriminate based on something more abstract, I can use the above trick[1], and voila, effectively nominal types!
[1] With or without the tag being there at runtime, depending on the situation, and actually I do have a lot of interface definitions that start with "classRef: " + some URI identifying the concept represented by this type. It's a URI because I like to think of everything as if I were describing it with RDF, you see.
(More of my own blathering on the subject from a few years ago here: http://www.nuke24.net/plog/32.html)
[+] [-] williamdclt|1 year ago|reply
[+] [-] comagoosie|1 year ago|reply
Equality can be problematic too. Imagine an Extension type, one could compare it with ".mp4" or "mp4", which one is correct?
Opaque types (that extend from `unknown` instead of T) work around these problems by forcing users through selector functions.
[+] [-] c-hendricks|1 year ago|reply
Also, since all examples of branded / nominal types in TypeScript use `as` (I assume to get around the fact that the object you're returning isn't actually of the shape you're saying it is...), you should read up on the pitfalls of it:
https://timdeschryver.dev/blog/stop-misusing-typescript-type...
https://www.reddit.com/r/typescript/comments/z8f7mf/are_ther...
https://web.archive.org/web/20230529162209/https://www.bytel...
[+] [-] OptionOfT|1 year ago|reply
I use the newtype pattern a lot in Rust. I try to avoid passing strings around. Extracting information over and over is cumbersome. Ensuring behavior is the same is big-ridden.
An example: is a string in the email address format? Parse it to an Email struct which is just a wrapper over String.
On top of that we then assign behavior to the type, like case insensitive equality. Our business requires [email protected] to be the same as [email protected]. This way we avoid the developer having to remember to do the checks manually every time. It's just baked into the equality of the type.
But in Rust using
just creates an alias. You really have to do something like Also, I know the only way to test an email address is to send an email and see if the user clicks a link. I reply should introduce a trait and a ValidatedEmail and a NotValidatedEmail.[+] [-] conaclos|1 year ago|reply
If I had to use branded types, I personally would prefer a different approach:
This also allows subtyping: Although this is sometimes convenient, I always find branded types too clever. I would prefer a dedicated syntax for nominal types. I made my own proposal: https://github.com/microsoft/TypeScript/issues/202#issuecomm...[+] [-] stevage|1 year ago|reply
One of the projects I work on is crying out for better support for nominal typing. It involves manipulating music notation, and there are so many things passed around that are all strings, but semantically different: a note ("A"), a pitched note ("A4"), a key ("A minor"), etc etc etc. Life would definitely be easier if I could just quickly and conveniently declare the types to be distinct.
I do use the branded types thing, but it's a bit clunky and sometimes gets in the way. And the error messages you get aren't as clear as you would like.
[+] [-] JonChesterfield|1 year ago|reply
It's called "nominal typing", because the types have names. I don't know why it's called "branded" instead here. There's probably a reason for the [syntax choice].
Old idea but a good one.
[+] [-] LelouBil|1 year ago|reply
Saying "nominal type" wouldn't mean anything.
[+] [-] plasticeagle|1 year ago|reply
[+] [-] 1sttimecaller|1 year ago|reply
Does deno catch this TS bug?
[+] [-] iainmerrick|1 year ago|reply
To do type-checking you need to run TSC.