Branded types for TypeScript(carlos-menezes.com)
158 points by carlos-menezes 11 days ago | 29 comments
bradrn 10 days ago
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.

beeboobaa3 10 days ago
> 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.

DanielHB 10 days ago
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).

quonn 10 days ago
The article describes making "number" a different type, not A and B. It's true that making A and B different is a unique problem of TypeScript, but making number a different type is a common issue in many languages.
DanielHB 10 days ago
number is a primitive, branding a primitive can be done like in the example. To brand a class you could also add a private field.

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

dunham 10 days ago
> For languages with nominal typing and primitives you need to box the type yes.

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.
robocat 10 days ago
Good article on using branded classes with Typescript to avoid structural typing:

https://prosopo.io/articles/typescript-branding/

discussion: https://news.ycombinator.com/item?id=40146751

MrJohz 10 days ago
You can fix that fairly easily using private variables:

  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`.
DanielHB 10 days ago
There is nothing to fix in my example, I was just highlighting the difference between nominal and structural typing. Adding a private field to the class is a form of branding (just like adding a Symbol key to a primitive).
MrJohz 10 days ago
The point is that Typescript does have nominal typing. It's used if a class is declared with any kind of private member, and for `unique symbol`s. So both in the case I showed, and the case shown in the article, we are using true nominal types.

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.

davorak 10 days ago
> which are true nominal typing.

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

ackfoobar 10 days ago
Without your example, I would've bet that TS uses structural typing for interfaces and nominal typing for classes.
DanielHB 9 days ago
I thought so too originally, was very surprised.
10 days ago
bradrn 10 days ago
Indeed, this is what I was trying to say!
DanielHB 10 days ago
yeah this is such a common misconception, but give the class example I showed and people just get it.

"structural typing" and "nominal typing" are still quite new terms for most devs

frenchy 10 days ago
> Branding makes structural typing work like nominal typing for the branded type only.

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.

deredede 10 days ago
> Classes exist at runtime

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.

nequo 10 days ago
Haskell implements type classes via dictionary passing that don’t always get optimized away by the compiler so it does have a slight 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...

deredede 9 days ago
My quotation was not good - I intended to reply to the argument that "nominal type do [exist at runtime] to some level". The newtype pattern in either Haskell or Rust is fully transparent at runtime.
garethrowlands 10 days ago
As others have said, types don't necessarily exist at runtime. Types allow reasoning about the program source without executing it. Java is more the exception than the rule here; conventionally compiled languages such as C don't usually have types at runtime.
skybrian 10 days ago
Depends what you mean by "most languages." I think it's clearer to say which languages have zero-overhead custom types.

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.

beeboobaa3 10 days ago
Java does not have it yet. Project Valhalla might bring it with Value types.
JonChesterfield 10 days ago
There's never any trace of typescript types at runtime.
aidos 10 days ago
By “trace” I think GP meant that the required wrapper is still there at runtime but was only in service of the type system.
mattstir 10 days ago
I think what they meant is that at runtime, you don't end up with objects that look something like:

  {
    "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.
mistercow 10 days ago
If you wrapped the value to give it a “brand”, the wrapper would still exist at runtime. The technique in the article avoids that.
recursive 9 days ago
Well, enums.
hn_throwaway_99 10 days ago
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.

RussianCow 10 days ago
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.
hn_throwaway_99 10 days ago
> Structural typing makes it too easy to accidentally pass something that looks like a duck but is actually a goose.

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.

sapling-ginger 9 days ago
All valid javascript programs are valid typescript programs, so typescript necessarily must have structural type, it cannot have nominal type without breaking compatibility with javascript, which is a hard requirement.
RussianCow 9 days ago
Correct. My comment was about programming languages in general and not specific to TypeScript. :)
cageface 9 days ago
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.

afiori 10 days ago
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.
mirekrusin 10 days ago
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.

int_19h 10 days ago
You can do classes, inheritance, and LSP with structural typing just fine; look at OCaml.
jacobsimon 10 days ago
You can still do this with classes in typescript:

class Hash extends String {}

https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...

brlewis 10 days ago
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.

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
mason55 10 days ago
Or, a version that's more inline with the post you're replying to.

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...
jacobsimon 9 days ago
Oops you’re right!
yencabulator 10 days ago
Great example of something that does not work. Javascript classes are structural by default, Typescript does nothing there.

https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...

mirekrusin 10 days ago
eru 9 days ago
Btw, there are quite a few language that have nominal typing, but use the equivalent of structural typing in their unions.

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 10 days ago
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.

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 10 days ago
> 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.

lolinder 10 days ago
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!

lIIllIIllIIllII 10 days ago
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.

kaoD 10 days ago
> 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'>;
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...

hombre_fatal 10 days ago
This is a far better summary of branded types than the top level comment that most people commenting should read before weighing in with their "why not just" solutions.
IshKebab 10 days ago
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.

stiiv 10 days ago
> A runtime bug is now a compile time bug.

This isn't valuable to you? How do you get this without nominal typing, especially of primatives?

tom_ 10 days ago
Did they edit their post? The > syntax indicates a verbatim quote here.
stiiv 9 days ago
Verbatim from OP.
tom_ 9 days ago
Oh, I see, it's from the article - right, got it.
skybrian 10 days ago
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.

jacobsimon 10 days ago
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:

class Hash extends String {}

compareHash(hash: Hash, input: string)

Here's an example: https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...

mason55 10 days ago
As mentioned elsewhere, what this is actually doing is showing that string and String are not structurally equivalent in TS.

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.

mattstir 10 days ago
> 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.

treflop 10 days ago
The real problem is that hashes as strings is wrong.

Hashes are typically numbers.

Do you store people's ages as hex strings?

mattstir 10 days ago
> Hashes are typically numbers

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.

vjerancrnjak 9 days ago
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.

Quothling 9 days ago
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.

vjerancrnjak 9 days ago
node's Buffer has a `toString` with arguments (probably should have been called encode).
10 days ago
kaoD 10 days ago
> 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!
[0] https://github.com/sindresorhus/type-fest/blob/main/source/o...

[1] https://www.typescriptlang.org/play/?#code/CYUwxgNghgTiAEYD2...

hombre_fatal 9 days ago
Finally someone writing practical examples instead of Animal / Dog / Cat.
arctek 9 days ago
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!
Animats 10 days ago
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.

kevincox 10 days ago
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.
earleybird 10 days ago
NASA might have some thoughts on mixing inch and meter types :-)

https://en.wikipedia.org/wiki/Mars_Climate_Orbiter

exceptione 10 days ago
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
mc10 10 days ago
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
exceptione 10 days ago
There are other places were you use types as well.

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.

stiiv 10 days ago
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.

anamexis 10 days ago
Can you say more about natively supporting discriminated unions?

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;
      }
    };
stiiv 10 days ago
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."

mc10 10 days ago
Aren't these two forms isomorphic:

    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.

anamexis 10 days ago
Isn’t it the same in TypeScript? You don’t need an explicit type field.
stiiv 10 days ago
It depends on what you're trying to achieve. If there are sufficient structural differences, you're fine (`"foo" in myThing` can discrimate) but if two types in your union have the same structure, TS doesn't give you a way to tell them apart. (This relates back to branded types.)

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.

anamexis 10 days ago
True, although I think that's just missing branded types, not discriminated unions.
mattstir 10 days ago
> 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.

stiiv 9 days ago
Big thank you for clarifying -- I missed that. This approach is far less unsavory that some other attempts that I've seen.
sleazy_b 10 days ago
I believe type-fest supports this, previously as “Opaque” types: https://github.com/sindresorhus/type-fest/blob/main/source/o...
jakubmazanec 10 days ago
True, although "Opaque" was deprecated and replaced with similar type "Tagged" (that supports multiple tags and metadata).
mpawelski 10 days ago
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.
freeney 10 days ago
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
chromakode 10 days ago
Alternatively for the case of id strings with known prefixes, a unique feature of TypeScript is you can use template string literal types:

https://www.kravchyk.com/adding-type-safety-to-object-ids-ty...

lolinder 10 days ago
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.
kaoD 10 days ago
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.
comagoosie 10 days ago
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?
dyeje 10 days ago
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.
10 days ago
10 days ago
culi 10 days ago
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
mattstir 10 days ago
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"?

kookamamie 10 days ago
These are sometimes called "strong" or phantom types in other languages, e.g.: https://github.com/mapbox/cpp/blob/master/docs/strong_types....
jweir 10 days ago
Elm makes great use of these, for example in the Units package:

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 10 days ago
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.

(More of my own blathering on the subject from a few years ago here: http://www.nuke24.net/plog/32.html)

williamdclt 10 days ago
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
TOGoS 10 days ago
According to Deno it does work.

  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.
comagoosie 10 days ago
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.

c-hendricks 10 days ago
Strings might not be the best way of demonstrating nominal typing, since that's already something TypeScript can manage: https://www.typescriptlang.org/play/?#code/C4TwDgpgBAEghgZwB...

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...

mattstir 10 days ago
I'm legitimately confused about why so many people in this thread are showing off template literal types as if actual hash functions just prepend "hash_" onto strings and call it a day. There are a lot of different types of data that don't have a predictable shape that TLTs just don't help with at all.

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.

10 days ago
OptionOfT 10 days ago
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 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.
kriiuuu 10 days ago
Scala 3 has opaque types for this. And libraries like iron build on top of it so you can have a very clean way of enforcing such things at compiletime.
conaclos 10 days ago
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 }

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...
AlienRobot 9 days ago
iirc this is why the hungarian notation existed. They had ints for columns and rows and they used a prefix on variable names to distinguish which ints were columns and which ints were rows. Though I may be misremembering.
stevage 9 days ago
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.

JonChesterfield 10 days ago
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].

Old idea but a good one.

LelouBil 10 days ago
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.

Saying "nominal type" wouldn't mean anything.

plasticeagle 10 days ago
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.
1sttimecaller 10 days ago
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?

Does deno catch this TS bug?

iainmerrick 9 days ago
No, neither Deno nor Bun does any type-checking. They both just convert the code to JS.

To do type-checking you need to run TSC.

rough-sea 9 days ago
iainmerrick 9 days ago
Oh! I stand corrected. That's a wrapper for TSC, though, right?

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.

rough-sea 8 days ago
"embeds TSC as a library" understates the integration:

https://github.com/denoland/deno/blob/75efc74931c1021fdc41c9...

https://github.com/denoland/deno/blob/75efc74931c1021fdc41c9...

Deno makes certain tradeoffs that make it feel seamless

iainmerrick 8 days ago
I'm now intrigued and might try it out. I like TypeScript but dislike TSC.
morning4coffe 9 days ago
Interesting approach to simulate nominal typing in TypeScript. It's like finding creative solutions to bridge the gap between structural and nominal typing. Nicely explained in the article!
AlienRobot 9 days ago
I'd understand if you were coding PHP without an IDE, but coding Typescript how do you get the argument order wrong?
herpdyderp 10 days ago
rednafi 9 days ago
It’s trivial to do with nominal type.

In Python:

```Hash = NewType('Hash', str)

def generate_hash(what: str) -> Hash: return Hash(f“hashed_{what}”)```

hombre_fatal 9 days ago
What makes it trivial here isn't nominal vs structural typing but rather that Python has a utility function for this case while you have to implement it yourself in Typescript with a one-liner.
9 days ago
KolmogorovComp 10 days ago
Is it erased at runtime? The article doesn’t mention this.
mattstir 10 days ago
It is "erased" in this example in the sense that the hashes are just strings at runtime, and not other objects instead. That's because the `generateHash` function in the article's example uses `as Hash` to tell the compiler "yes, I know I'm only returning a string here, but trust me, this is actually a `string & { [__brand]: 'Hash' }`".

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.

hombre_fatal 9 days ago
What's nice here is that `makeHash: Hash = (x: string) => x as Hash` checks that `x` actually overlaps with the branded type (string, in this case).

At first it looks like you could just lie and say `x as User` or something but it's not the case.

lolinder 10 days ago
Yes. In TypeScript, everything placed in type signatures or type definitions is erased at runtime.
Swiffy0 9 days ago
This to me seems like one of those things that makes me think why is this considered such a problem that it needs a "proper" solution like this.

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.

throw156754228 10 days ago
Feels hacky as hell, with the string literal in there.
IshKebab 10 days ago
The string literal doesn't exist at runtime.
throw156754228 10 days ago
I know. I'm talking about its lack of elegance.
IshKebab 10 days ago
Strings literals are proper singleton types in Typescript. It's not inelegant at all.
recursive 9 days ago
You could use a unique symbol as well.
foooorsyth 10 days ago
Really ugly way to avoid something already solved by named parameters and/or namespacing
demurgos 10 days ago
This is more about nominal VS structural typing. I don't see how named parameters or namespacing would prevent accidental structural type matches.
culi 10 days ago
TypeScript is a structurally typed language on purpose. But even then nominal features already exist that would have solved this problem much more elegantly. Such as `unique symbol`

https://www.typescriptlang.org/docs/handbook/symbols.html#un...

mattstir 10 days ago
Did you... read the article?
beeboobaa3 10 days ago
You might need to read the article again, because those things are unrelated. Or you should explain what you mean.
foooorsyth 10 days ago
I read the article, thanks.

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).

aidos 10 days ago
Respectfully disagree on the elegance. This looks pretty neat to me:

    type Hash = Branded<string, "Hash">;
beeboobaa3 10 days ago
The point of branded types is, among other things, that you do not need to introduce a wrapper class which consumes additional memory.
golergka 10 days ago
Are you worried about compiler memory consumption? Because it's not a class, it's a type, and it's erased at compile time.
beeboobaa3 10 days ago
Runtime. Not compiler.
golergka 10 days ago
None of this exists at runtime.
hombre_fatal 10 days ago
Well, they're talking about alternative solutions that do exist at runtime like wrappers.

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'.

throw156754228 10 days ago
Really? I'm surprised you mention memory is even a consideration, never even heard it raised as far as typing choices are concerned.
beeboobaa3 10 days ago
(More than) doubling the memory required for all of your integers would be silly. You could use `{userId: 12345}` everywhere, or you can use a branded type and it's just `12345` at runtime.
10 days ago