[HN Gopher] Branded types for TypeScript
       ___________________________________________________________________
        
       Branded types for TypeScript
        
       Author : carlos-menezes
       Score  : 125 points
       Date   : 2024-05-14 10:21 UTC (1 days ago)
        
 (HTM) web link (www.carlos-menezes.com)
 (TXT) w3m dump (www.carlos-menezes.com)
        
       | foooorsyth wrote:
       | Really ugly way to avoid something already solved by named
       | parameters and/or namespacing
        
         | demurgos wrote:
         | This is more about nominal VS structural typing. I don't see
         | how named parameters or namespacing would prevent accidental
         | structural type matches.
        
           | culi wrote:
           | 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 wrote:
             | Did you... read the article?
        
         | beeboobaa3 wrote:
         | You might need to read the article again, because those things
         | are unrelated. Or you should explain what you mean.
        
           | foooorsyth wrote:
           | 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 wrote:
             | Respectfully disagree on the elegance. This looks pretty
             | neat to me:                   type Hash = Branded<string,
             | "Hash">;
        
             | beeboobaa3 wrote:
             | The point of branded types is, among other things, that you
             | do not need to introduce a wrapper class which consumes
             | additional memory.
        
               | throw156754228 wrote:
               | Really? I'm surprised you mention memory is even a
               | consideration, never even heard it raised as far as
               | typing choices are concerned.
        
               | beeboobaa3 wrote:
               | (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.
        
               | golergka wrote:
               | 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 wrote:
               | Runtime. Not compiler.
        
               | golergka wrote:
               | None of this exists at runtime.
        
               | hombre_fatal wrote:
               | 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'.
        
       | bradrn wrote:
       | 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 wrote:
         | > 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.
        
           | JonChesterfield wrote:
           | There's never any trace of typescript types at runtime.
        
             | mistercow wrote:
             | If you wrapped the value to give it a "brand", the wrapper
             | would still exist at runtime. The technique in the article
             | avoids that.
        
             | aidos wrote:
             | 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 wrote:
             | 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.
        
           | DanielHB wrote:
           | 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).
        
             | bradrn wrote:
             | Indeed, this is what I was trying to say!
        
               | DanielHB wrote:
               | 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
        
             | quonn wrote:
             | 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 wrote:
               | 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 wrote:
               | > 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.
        
             | MrJohz wrote:
             | 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 wrote:
               | 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 wrote:
               | 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 wrote:
               | > 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 wrote:
               | Without your example, I would've bet that TS uses
               | structural typing for interfaces and nominal typing for
               | classes.
        
             | frenchy wrote:
             | > 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 wrote:
               | > 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.
        
               | garethrowlands wrote:
               | 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.
        
             | robocat wrote:
             | 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
        
           | skybrian wrote:
           | 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 wrote:
             | Java does not have it yet. Project Valhalla might bring it
             | with Value types.
        
         | afiori wrote:
         | 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 wrote:
           | 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 wrote:
             | You can do classes, inheritance, and LSP with structural
             | typing just fine; look at OCaml.
        
         | jacobsimon wrote:
         | You can still do this with classes in typescript:
         | 
         | class Hash extends String {}
         | 
         | https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
        
           | brlewis wrote:
           | 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/GYVwd
           | gxgLglg9mABO...                 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 wrote:
             | 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/MYGwhgzhAEASkAto
             | F...
        
           | yencabulator wrote:
           | 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 wrote:
             | and works correctly in flow [0]
             | 
             | [0] https://flow.org/try/#1N4Igxg9gdgZglgcxALlAIwIZoKYBsD6u
             | EEAzt...
        
         | hn_throwaway_99 wrote:
         | 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.
        
       | sleazy_b wrote:
       | I believe type-fest supports this, previously as "Opaque" types:
       | https://github.com/sindresorhus/type-fest/blob/main/source/o...
        
         | jakubmazanec wrote:
         | True, although "Opaque" was deprecated and replaced with
         | similar type "Tagged" (that supports multiple tags and
         | metadata).
        
       | JonChesterfield wrote:
       | 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 wrote:
         | 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.
        
       | chromakode wrote:
       | 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...
        
         | comagoosie wrote:
         | 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?
        
         | lolinder wrote:
         | 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 wrote:
           | 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.
        
       | freeney wrote:
       | 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
        
       | msoad wrote:
       | 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...
        
         | skybrian wrote:
         | 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 wrote:
           | 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 wrote:
             | 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.
        
         | stiiv wrote:
         | > 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_ wrote:
           | Did they edit their post? The > syntax indicates a verbatim
           | quote here.
        
         | dllthomas wrote:
         | > 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.
        
         | lIIllIIllIIllII wrote:
         | 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 wrote:
           | > 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/C4TwDgpgBAEghg
           | ZwB...
        
             | hombre_fatal wrote:
             | 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 wrote:
           | 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.
        
         | lolinder wrote:
         | 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!
        
         | kaoD wrote:
         | > 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...
        
         | mattstir wrote:
         | > 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 wrote:
           | The real problem is that hashes as strings is wrong.
           | 
           | Hashes are typically numbers.
           | 
           | Do you store people's ages as hex strings?
        
             | mattstir wrote:
             | > 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.
        
       | mpawelski wrote:
       | 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.
        
       | herpdyderp wrote:
       | related recent discussion:
       | https://news.ycombinator.com/item?id=40146751 (about
       | https://prosopo.io/articles/typescript-branding/)
        
       | OptionOfT wrote:
       | 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 wrote:
         | 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.
        
       | comagoosie wrote:
       | 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.
        
       | kookamamie wrote:
       | These are sometimes called "strong" or phantom types in other
       | languages, e.g.:
       | https://github.com/mapbox/cpp/blob/master/docs/strong_types....
        
         | jweir wrote:
         | 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...
        
       | stiiv wrote:
       | 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 wrote:
         | 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 wrote:
           | 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."
        
             | anamexis wrote:
             | Isn't it the same in TypeScript? You don't _need_ an
             | explicit type field.
        
               | stiiv wrote:
               | 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 wrote:
               | True, although I think that's just missing branded types,
               | not discriminated unions.
        
             | mc10 wrote:
             | 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.
        
         | mattstir wrote:
         | > 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.
        
       | KolmogorovComp wrote:
       | Is it erased at runtime? The article doesn't mention this.
        
         | lolinder wrote:
         | Yes. In TypeScript, everything placed in type signatures or
         | type definitions is erased at runtime.
        
         | mattstir wrote:
         | 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.
        
       | 1sttimecaller wrote:
       | 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?
        
       | c-hendricks wrote:
       | 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 wrote:
         | 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.
        
       | throw156754228 wrote:
       | Feels hacky as hell, with the string literal in there.
        
         | IshKebab wrote:
         | The string literal doesn't exist at runtime.
        
           | throw156754228 wrote:
           | I know. I'm talking about its lack of elegance.
        
             | IshKebab wrote:
             | Strings literals are proper singleton types in Typescript.
             | It's not inelegant at all.
        
       | TOGoS wrote:
       | 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 wrote:
         | 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 wrote:
           | 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.
        
       | Animats wrote:
       | 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.
        
         | earleybird wrote:
         | NASA might have some thoughts on mixing inch and meter types
         | :-)
         | 
         | https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
        
         | kevincox wrote:
         | 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.
        
         | exceptione wrote:
         | 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 wrote:
           | 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 wrote:
             | 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.
        
       | dyeje wrote:
       | 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.
        
       | culi wrote:
       | 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 wrote:
         | 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"?
        
       | plasticeagle wrote:
       | 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.
        
       | conaclos wrote:
       | 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...
        
       ___________________________________________________________________
       (page generated 2024-05-15 23:01 UTC)