Back

Branded types for TypeScript

100 points1 daycarlos-menezes.com
Animats2 hours 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.

kevincox1 hour 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.
exceptione40 minutes 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
mc1037 minutes 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
earleybird1 hour ago

NASA might have some thoughts on mixing inch and meter types :-)

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

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

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

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

robocat47 minutes 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

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

+1
DanielHB7 hours ago
MrJohz7 hours 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`.
+1
DanielHB7 hours ago
frenchy5 hours 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.

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

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

bradrn7 hours ago

Indeed, this is what I was trying to say!

DanielHB7 hours 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

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

beeboobaa34 hours ago

Java does not have it yet. Project Valhalla might bring it with Value types.

JonChesterfield8 hours ago

There's never any trace of typescript types at runtime.

mattstir36 minutes 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.
aidos7 hours 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.

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

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

mirekrusin1 hour 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_19h41 minutes ago

You can do classes, inheritance, and LSP with structural typing just fine; look at OCaml.

jacobsimon6 hours ago

You can still do this with classes in typescript:

class Hash extends String {}

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

brlewis6 hours 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
mason556 hours 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...
yencabulator6 hours 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...

mirekrusin1 hour ago
dyeje31 minutes 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.

neverokay12 minutes ago

I honestly think excessive Typescript is a symptom of certain flow states induced by adhd medication. You just become obsessed about making these egregious and often grotesquely verbose types (anyone that abused this stuff might relate to how perfectionist and ocd you can become on it, and Typescritp condenses scream at the top of their lungs that the people writing it are loaded). You’re also high, so you think your code is amazing lol.

It’s genuinely disgusting.

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

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

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

lolinder6 hours 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!

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

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

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

stiiv7 hours 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_2 hours ago

Did they edit their post? The > syntax indicates a verbatim quote here.

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

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

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

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

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

sleazy_b8 hours ago

I believe type-fest supports this, previously as “Opaque” types: https://github.com/sindresorhus/type-fest/blob/main/source/o...

jakubmazanec7 hours ago

True, although "Opaque" was deprecated and replaced with similar type "Tagged" (that supports multiple tags and metadata).

freeney7 hours 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

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

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

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

comagoosie7 hours 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?

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

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

anamexis6 hours 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;
      }
    };
stiiv5 hours 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."

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

anamexis5 hours ago

Isn’t it the same in TypeScript? You don’t need an explicit type field.

+1
stiiv4 hours ago
kookamamie7 hours ago

These are sometimes called "strong" or phantom types in other languages, e.g.: https://github.com/mapbox/cpp/blob/master/docs/strong_types....

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

c-hendricks5 hours 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...

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

OptionOfT7 hours 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.
kriiuuu7 hours 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.

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

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

herpdyderp7 hours ago
1sttimecaller6 hours 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?

throw1567542282 hours ago

Feels hacky as hell, with the string literal in there.

IshKebab2 hours ago

The string literal doesn't exist at runtime.

throw1567542282 hours ago

I know. I'm talking about its lack of elegance.

IshKebab1 hour ago

Strings literals are proper singleton types in Typescript. It's not inelegant at all.

TOGoS2 hours 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)

williamdclt59 minutes 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

KolmogorovComp7 hours ago

Is it erased at runtime? The article doesn’t mention this.

lolinder6 hours ago

Yes. In TypeScript, everything placed in type signatures or type definitions is erased at runtime.

foooorsyth8 hours ago

Really ugly way to avoid something already solved by named parameters and/or namespacing

demurgos8 hours ago

This is more about nominal VS structural typing. I don't see how named parameters or namespacing would prevent accidental structural type matches.

beeboobaa38 hours ago

You might need to read the article again, because those things are unrelated. Or you should explain what you mean.

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

aidos7 hours ago

Respectfully disagree on the elegance. This looks pretty neat to me:

    type Hash = Branded<string, "Hash">;
beeboobaa36 hours ago

The point of branded types is, among other things, that you do not need to introduce a wrapper class which consumes additional memory.

+1
golergka44 minutes ago
+1
throw1567542282 hours ago