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.
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).
Some languages all values are objects and in those languages then the branding argument applies the same way. For languages with nominal typing and primitives you need to box the type yes. Regardless the core of the issue is understanding how structural typing works vs nominal typing
But the compiler can elide the box for you. Haskell and Idris do this.
Haskell's newtype gives a nominal wrapper around a type without (further) boxing at at runtime. It is erased at compile time. Haskell does box their primitives, but via optimization they are used unboxed in some cases (like inside the body of a function). This technique could be applied to a language that doesn't box its primitives.
Idris also does this for any type that is shaped like a newtype (one data constructor, one argument). In that case, both on the scheme and javascript backend, a newtyped Int are represented as "bare" numbers. E.g. with:
type Foo = MkFoo Int
a `MkFoo 5` value is just `5` in the generated javascript or scheme code.https://prosopo.io/articles/typescript-branding/
discussion: https://news.ycombinator.com/item?id=40146751
class A {
private value: number
}
class B {
private value: number
}
const x: A = new B() // error
You can also use the new Javascript private syntax (`#value`). And you can still have public values that are the same, so if you want to force a particular class to have nominal typing, you can add an unused private variable to the class, something like `private __force_nominal!: void`.In fairness, we're also using branded types, which I think is confusing the matter here*. But they are specifically branded nominal types. We can also create structurally-typed brands (before the `unique symbol` technique, that was the only possible option). I think that's what the previous poster was referring to by "simulated nominal typing" — this is distinct from using `unique type` and private members, which are true nominal typing.
* Note: Branded types aren't necessarily a well-defined thing, but for the sake of the discussion let's define them so: a branded type is a type created by adding attributes to a type that exist at compile time but not at runtime.
One part that was not clear to me without testing, and since I do not use typescript regularly, was that you only get nominal typing between the classes that share the private member and if you start going out side that set you lose nominal typing. So you do not get a nominal type, but you can get a subset of types that when interacting with each other act as if they were nominal types.
So class Cat that uses `private __force_nominal!: void` can still be used as class Dog if Dog does not have `private __force_nominal!: void`.
Example[1]:
class Dog {
breed: string
constructor(breed: string) {
this.breed = breed
}
}
function printDog(dog: Dog) {
console.log("Dog: " + dog.breed)
}
class Cat {
private __force_nominal!: string
breed: string
constructor(breed: string) {
this.breed = breed
}
}
const shasta = new Cat("Maine Coon")
printDog(shasta)
edit - the above type checks in typescript 5.4.5[1] modified example from https://asana.com/inside-asana/typescript-quirks
flowtype documentaiton: https://flow.org/en/docs/lang/nominal-structural/#toc-object...
Example: https://flow.org/try/#1N4Igxg9gdgZglgcxALlAIwIZoKYBsD6uEEAzt...
"structural typing" and "nominal typing" are still quite new terms for most devs
That's not quite true. Branding doesn't exist at run time, where as nominal typing usually does at some level. Classes exist at runtime, but most typescript types don't, so unless there's something specific about the shape of the data that you can check with a type guard, it's impossible to narrow the type.
Not necessarily, depending on the language. Functional languages and system languages such as OCaml, Haskell, Rust, but also C (painfully) and C++ can represent wrapper types within a nominal type system at no runtime cost.
https://okmij.org/ftp/Computation/typeclass.html#dict
In Rust, using trait objects also generates a vtable for dynamic dispatch so in that case traits are not fully erased:
https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/sh...
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.
{
"brand": "Hash",
"value": "..."
}
which would be the case if you used the more obvious wrapper route. Using this branding approach, the branded values are exactly the same at runtime as they would be if they weren't branded.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:
// 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.This may be true, but in reality after 7 years of using TypeScript I don't think I've ever encountered this as a bug for an object (Record) type.
Even for branded types I find the value much more in self-documenting code than actual type safety, and I only have used branded types for primitives.
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...
class Animal {
isJaguar: boolean = false;
}
class Automobile {
isJaguar: boolean = false;
}
function engineSound(car: Automobile) {
return car.isJaguar ? "vroom" : "put put";
}
console.log(engineSound(42)); // TypeScript complains
console.log(engineSound(new Animal())); // TypeScript does not complain
Just add an Email class that also extends String and you can see that you can pass an Email to the compareHash function without it complaining.
class Hash extends String {}
class Email extends String {}
// Ideally, we only want to pass hashes to this function
const compareHash = (hash: Hash, input: string): boolean => {
return true;
};
const generateEmail = (input: string): Email => {
return new Email(input);
}
// Example usage
const userInput = "secretData";
const email = generateEmail(userInput);
// Whoops, we passed an email as a hash and TS doesn't complain
const matches = compareHash(email, userInput);
https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
[0] https://flow.org/try/#1N4Igxg9gdgZglgcxALlAIwIZoKYBsD6uEEAzt...
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]):
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'>;
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...
If you add another class Email that extends String, you can pass it as a Hash without any problems. And you can get rid of the Hash stuff altogether and do something like
compareHash(userInput, new String(userInput));
and that fails just as well as the Hash example.Using extends like this doesn't actually fix the problem for real.
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?
If we want to get really pedantic, hashes are typically sequences of bytes, not a single number, so really `UInt8Array` is obviously the best choice here. It wouldn't fix the whole "getting arguments with the same types swapped around" issue though. Without named parameters, you have to pull out some hacks involving destructuring objects or branded types like these.
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:
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!
[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:
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.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
val is_admissible : min_age:int -> max_age:int -> bool
Also, if you later decide that an Age should not be int, but a string, you wont miss it in refactoring whereas in your example you don't have that facility.
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:
type MyUnion = { type: "foo"; foo: string } | { type: "bar"; bar: string };
And this will compile: (u: MyUnion) => {
switch (u.type) {
case "foo":
return u.foo;
case "bar":
return u.bar;
}
};
Whereas this wont: (u: MyUnion) => {
switch (u.type) {
case "foo":
return u.bar;
case "bar":
return u.foo;
}
};
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."
type MyUnion = { type: "foo"; foo: string } | { type: "bar"; bar: string };
vs type MyUnion = Foo of { foo: string } | Bar of { bar: string };
You still need some runtime encoding of which branch of the union your data is; otherwise, your code could not pick a branch at runtime.There's a slight overhead to the TypeScript version (which uses strings instead of an int to represent the branch) but it allows discriminated unions to work without having to introduce them as a new data type into JavaScript. And if you really wanted to, you could use a literal int as the `type` field instead.
A good example would be `type Money = Dollars | Euros` where both types in the union alias `number`. You need a tag. In other languages, you don't.
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...
How would you "approach the problem from the way the language lends itself to be solved"?
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...
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.
(More of my own blathering on the subject from a few years ago here: http://www.nuke24.net/plog/32.html)
type USD = number & { currency?: "USD" }
type CAD = number & { currency?: "CAD" }
const fiveUsd : USD = 5;
const fiveCad : CAD = fiveUsd;
console.log("How many CAD? " + fiveCad);
Results in an error, Type '"USD"' is not assignable to type '"CAD"'.
If by "doesn't work" you mean that the implicit number->USD conversion is allowed, then I disagree with the judgement, as that is by design. But once the values are in variables of more specific types, the type-checker will catch it.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:
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...
While the pitfalls of mindlessly slapping `as XYZ` on lines to make it compile certainly exist (when the type definition changes without the areas with `as` being updated, etc), I don't know if branded values are really the place where they pop up. You brand a primitive when you want to differentiate it from other primitives that otherwise have the same underlying type. In that scenario, you can't really change the definition of the underlying primitive, so you can't really run into issues there.
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 foo@BAR.com to be the same as FoO@bAr.com. 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.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 }
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...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.
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.
Saying "nominal type" wouldn't mean anything.
Does deno catch this TS bug?
To do type-checking you need to run TSC.
Edit to add: I see, Deno embeds TSC as a library and tries to straighten out the mess of tsconfig. That must be a leaky abstraction, which makes me wary, but maybe it works well in practice.
https://github.com/denoland/deno/blob/75efc74931c1021fdc41c9...
https://github.com/denoland/deno/blob/75efc74931c1021fdc41c9...
Deno makes certain tradeoffs that make it feel seamless
In Python:
```Hash = NewType('Hash', str)
def generate_hash(what: str) -> Hash: return Hash(f“hashed_{what}”)```
That `as Hash` is known as a type assertion in Typescript and is normally used when the developer knows something info about the code that Typescript can't.
At first it looks like you could just lie and say `x as User` or something but it's not the case.
If I have a function written in TS which takes a string type parameter called a hash... isn't it already obvious what the function wants?
Furthermore when the function in question is a hash checking function, it is working exactly as intended when it returns something like "invalid hash" when there is a problem with the hash. You either supply it a valid well formed hash and the function returns success, or you supply it any kind of non-valid hash and the function returns a failure. What is the problem?
In case the function is not a hash checking function per se, but e.g. a function which uses the hash for someyhing, you will still need to perform some checks on the hash before using it. Or it could be a valid hash, but there is nothing to be found with that hash, in which case once again everything already works exactly as it should and you get nothing back.
It's like having a function checkBallColor which wants you to supply a "ball" to it. Why would you need to explicitly define that you need to give it a ball with a color property in which the color is specified in a certain way? If someone calls that function with a ball that has a malformed color property, then the function simply returns that the ball's color property is malformed. You will, in most cases probably, have to check the color property in runtime anyway.
I've used TS based graphics libraries and they often come with something like setColorHex, setColorRGB, etc. functions so that you know how the color should be given. If you supply the color in a wrong way, nothing happens and I think that is just fine.
Sorry for the rant, but I just don't get some of these problems. Like... you either supply a valid hash and all is fine, or you don't and then you simply figure out why isn't it valid, which you will have to do with this branding system as well.
https://www.typescriptlang.org/docs/handbook/symbols.html#un...
It makes the claims that (1) a string of a specific form (a hash) could be misused (eg. someone might call toUpper on it) or (2) passed in incorrect order to a function that takes multiple strings.
Named parameters / outward-facing labels (Swift) completely solves (2). For (1), the solution is just ugly. Just use the type system in a normal manner and make a safe class “Hash” that doesn’t allow misuse. And/or use namespacing. And/or use extensions on String. And/or add a compiler extension that looks like outward-facing labels but for return types. So many cleaner options than this nasty brand (yes, that’s the point of the article, but the solution is still hideous. Make it elegant).
type Hash = Branded<string, "Hash">;
Granted, it's hard to know exactly what solutions the person is pitching (the person they're responding to). This person presumably thinks renaming arguments like foo(string:) to foo(hash:) solves branded types. And then they vaguely gesture at other solutions like namespacing and 'safe classes'.