Live data from Hacker News

Branded types for TypeScript

carlos-menezes.com

81–90 of 152 posts

Re: Branded types for TypeScript

#81

Earlier quoted context omitted.

You can still do this with classes in typescript: class Hash extends String {} https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...

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

and works correctly in flow [0]

[0] https://flow.org/try/#1N4Igxg9gdgZglgcxALlAIwIZoKYBsD6uEEAzt...

Re: Branded types for TypeScript

#82
post #72

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.

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 which is equivalent to Result 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.

Re: Branded types for TypeScript

#83
post #71

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 structu…

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

Re: Branded types for TypeScript

#84
post #57
post #45

Earlier quoted context omitted.

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-type…

> 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 type…

Without your example, I would've bet that TS uses structural typing for interfaces and nominal typing for classes.

Re: Branded types for TypeScript

#85

Earlier quoted context omitted.

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

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…

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

Re: Branded types for TypeScript

#86

Earlier quoted context omitted.

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…

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

Are you worried about compiler memory consumption? Because it's not a class, it's a type, and it's erased at compile time.

Re: Branded types for TypeScript

#87
post #31

Earlier quoted context omitted.

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.

Nominal typing with correct variance describes OO (classes, inheritance and rules governing it >). 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.

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

Re: Branded types for TypeScript

#88
post #72

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.

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

Re: Branded types for TypeScript

#89
post #72

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.

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 sema…

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

Re: Branded types for TypeScript

#90

Earlier quoted context omitted.

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

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

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.
Post reply on HN