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