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.
I see where you are coming from but you are not quite understanding what the OP was saying
class A {
public value: number
}
class B {
public value: number
}
const x: A = new B() // no error
This is structural typing (shape defines type), if typescript had nominal typing (name defines type) this would give an error. You could brand these classes to forcefully cause this to error.
Branding makes structural typing work like nominal typing for the branded type only.
It is more like "doing what this article describes" is the default behaviour of most languages (most languages use nominal typing).
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.
I think proper type inference along with some sort of struct literal syntax could make a lot of these problems go away without the need for structural typing. If you can create structs without explicitly mentioning the type (as long as it can be inferred), then you can use the same syntax for different types as long as their signatures are the same. Structural typing makes it too easy to accidentally pass something that looks like a duck but is actually a goose.
As an example, this syntax should be possible without structural typing or explicitly specifying the type of the value:
// assuming a type signature of moveToPoint(Point)
moveToPoint({x: 5, y: 10}) // the struct literal is inferred to be a Point
I believe F# has syntax like this, but it's been a while since I've used it so I don't remember the details.
Overall I do like working with Typescript and structural typing does have some real advantages but, like just about everything in engineering, there are also tradeoffs.
Without some discipline you can create some very messy type spaghetti with TS. Nominal typing is more rigid but it can also force you to think more clearly about and carefully define the different entities in your system.
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.
Nominal typing with correct variance describes OO (classes, inheritance and rules governing it <<liskov substitution principles, L from SOLID>>). Structural typing is used for everything else in js.
Flow does it correctly. Typescript treats everything as structurally typed.
As a side note flow also has first class support for opaque types so no need to resort to branding hacks.
That's distinguishing the String class from primitive string. I don't think that would still work with another `extends String` the same shape as Hash.
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.
> I also don't really get what this branded type adds beyond the typical way of doing it
Your example is a (non-working) tagged union, not a branded type.
Not sure about op's specific code, but good branded types [0]:
1. Unlike your example, they actually work (playground [1]):
type Hash = string & { tag: "hash" }
const doSomething = (hash: Hash) => true
doSomething('someHash') // how can I even build the type !?!?
2. Cannot be built except by using that branded type -- they're actually nominal, unlike your example where I can literally just add a `{ tag: 'hash' }` prop (or even worse, have it in a existing type and pass it by mistake)
3. Can have multiple brands without risk of overlap (this is also why your "wrap the type" comment missed the point, branded types are not meant to simulate inheritance)
4. Are compile-time only (your `tag` is also there at runtime)
5. Can be composed, like this:
type Url = Tagged<string, 'URL'>;
type SpecialCacheKey = Tagged<Url, 'SpecialCacheKey'>;
This is a bit of a nitpick because strings often aren't the most appropriate type for hashes... but in some cases I can see using strings as the best choice. It's probably the fastest option for one.
In any case it still demonstrates the usefulness of branded types.
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.
The demo they posted demonstrates how to do it. But I don’t think it’s a generally good solution to the problem, it feels like it solves this specific case where the type is a string hash. I think the evolution of this for other types and objects is more like what the OP article suggests.
I wonder if a more natural solution would be to extend the String class and use that to wrap/guard things:
> 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.
This is sort of by design. Generally speaking Object.prototype.toString() does not accept parameters, I think the only "standard" implementation which takes parameters is Number.prototype.toString(). You can overwrite .toString with your customized functions, but doing so is full of risks which can basiclaly be summed up to performance overhead and unexpected behaviour due to how you're not really in control of an object's state... As you sort of point out here.
I know it's very tempting for people coming from OOP languages to use their own custom toString functions, but you really shouldn't. If you really need a string version of an object for debugging purposes you should instead get it through JSON.stringify. This is partly because .toString() isn't really meant to be used by you in JS. You can, and in a few cases it may make sense, but it's usually unnecessary because JS will do it automatically if you simply wrap your non-string primitives in the string where you want to use them.
In general it's better to work with objects directly and not think of them as "classes". I think (and this is my opinion which people are going to disagree with) in general you're far better off by very rarely using classes at all in TS. There are obviously edge cases where you're going to need classes, but for 95% of your code they are going to be very unnecessary and often make it much harder for developers who may not primarily work with JS or another weakly typed language. Part of this is because classes aren't actually classes, but mainly it's because you can almost always achieve what you want with an interface or even a Type in a manner that is usually more efficient, more maintainable and easier to test because of it's decoupled nature. I have this opinion after working with JS in both the back-end and front-end for over a decade and seeing how horrible things can go wrong because we all write shitty code on a thursday afternoon, and because JS often won't work like many people from C#, Java or similar backgrounds might expect.
> 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!
Thanks for the examples, I'm working on a TypeScript code base at the moment and this is fantastic way of adding compile-time typing across many of the basic types I'm using!
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
The AgeMin/AgeMax example seems more of a deficiency due to a lack of named function parameters; it would be equally clear if it had a type signature (using OCaml as an example) of
val is_admissible : min_age:int -> max_age:int -> bool
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.
Sure! You need a `type` field (or something like it) in TS.
You don't need that in a language like F# -- the discrimation occurs strictly in virtue of your union definition. That's what I meant by "native support."
> 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.
This is why I always use `Math.random().toString(16)` for my examples :D People often get lost on the details, but they see `Math.random()` and they instantly get it's... well, just a random thing.
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?
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.
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.
Branding makes structural typing work like nominal typing for the branded type only.
It is more like "doing what this article describes" is the default behaviour of most languages (most languages use nominal typing).
Go has it. Java didn't used to have it so you would use wrapper classes, but I haven't kept up with Java language updates.
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.
As an example, this syntax should be possible without structural typing or explicitly specifying the type of the value:
I believe F# has syntax like this, but it's been a while since I've used it so I don't remember the details.Without some discipline you can create some very messy type spaghetti with TS. Nominal typing is more rigid but it can also force you to think more clearly about and carefully define the different entities in your system.
Flow does it correctly. Typescript treats everything as structurally typed.
As a side note flow also has first class support for opaque types so no need to resort to branding hacks.
class Hash extends String {}
https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
For example: https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABO...
https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
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?
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...
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.
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!
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.
Your example is a (non-working) tagged union, not a branded type.
Not sure about op's specific code, but good branded types [0]:
1. Unlike your example, they actually work (playground [1]):
2. Cannot be built except by using that branded type -- they're actually nominal, unlike your example where I can literally just add a `{ tag: 'hash' }` prop (or even worse, have it in a existing type and pass it by mistake)3. Can have multiple brands without risk of overlap (this is also why your "wrap the type" comment missed the point, branded types are not meant to simulate inheritance)
4. Are compile-time only (your `tag` is also there at runtime)
5. Can be composed, like this:
See my other comment for more on what a complete branded type offers https://news.ycombinator.com/item?id=40368052[0] https://github.com/sindresorhus/type-fest/blob/main/source/o...
[1] https://www.typescriptlang.org/play/?#code/C4TwDgpgBAEghgZwB...
In any case it still demonstrates the usefulness of branded types.
This isn't valuable to you? How do you get this without nominal typing, especially of primatives?
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.
I wonder if a more natural solution would be to extend the String class and use that to wrap/guard things:
class Hash extends String {}
compareHash(hash: Hash, input: string)
Here's an example: https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
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.
Hashes are typically numbers.
Do you store people's ages as hex strings?
Deleted Comment
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.
I know it's very tempting for people coming from OOP languages to use their own custom toString functions, but you really shouldn't. If you really need a string version of an object for debugging purposes you should instead get it through JSON.stringify. This is partly because .toString() isn't really meant to be used by you in JS. You can, and in a few cases it may make sense, but it's usually unnecessary because JS will do it automatically if you simply wrap your non-string primitives in the string where you want to use them.
In general it's better to work with objects directly and not think of them as "classes". I think (and this is my opinion which people are going to disagree with) in general you're far better off by very rarely using classes at all in TS. There are obviously edge cases where you're going to need classes, but for 95% of your code they are going to be very unnecessary and often make it much harder for developers who may not primarily work with JS or another weakly typed language. Part of this is because classes aren't actually classes, but mainly it's because you can almost always achieve what you want with an interface or even a Type in a manner that is usually more efficient, more maintainable and easier to test because of it's decoupled nature. I have this opinion after working with JS in both the back-end and front-end for over a decade and seeing how horrible things can go wrong because we all write shitty code on a thursday afternoon, and because JS often won't work like many people from C#, Java or similar backgrounds might expect.
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...
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.
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.https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
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: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.
You can already do this:
And this will compile: Whereas this wont:You don't need that in a language like F# -- the discrimation occurs strictly in virtue of your union definition. That's what I meant by "native support."
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.
https://www.kravchyk.com/adding-type-safety-to-object-ids-ty...