10 ms·
In most languages, doing what this article describes is quite straightforward: you would just define a new type (/ struct / class) called ‘Hash’, which function
by bradrn 2y ago
In most languages, doing what this article describes is quite straightforward: you would just define a new type (/ struct / class) called ‘Hash’, which functions can take or return. The language automatically treats this as a completely new type. This is called ‘nominal typing’: type equality is based on the name of the type.
The complication with TypeScript is that it doesn’t have nominal typing. Instead, it has ‘structural typing’: type equality is based on what the type contains. So you could define a new type ‘Hash’ as a string, but ‘Hash’ would just be a synonym — it’s still considered interchangeable with strings. This technique of ‘branded types’ is simply a way to simulate nominal typing in a structural context.
- beeboobaa3 2y ago> In most languages, doing what this article describes is quite straightforward Well, no. In most languages you wind up making a typed wrapper object/class that holds the primitive. This works fine, you can just do that in TypeScript too. The point of branded types is that you're not introducing a wrapper class and there is no trace of this brand at runtime.
- JonChesterfield 2y agoThere's never any trace of typescript types at runtime.
- mistercow 2y agoIf you wrapped the value to give it a “brand”, the wrapper would still exist at runtime. The technique in the article avoids that.
- aidos 2y agoBy “trace” I think GP meant that the required wrapper is still there at runtime but was only in service of the type system.
- mattstir 2y agoI 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.
- recursive 2y agoWell, enums.
- DanielHB 2y agoI 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 2y agoIndeed, this is what I was trying to say!
- DanielHB 2y agoyeah 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
- deleted 2y ago[deleted]
- quonn 2y agoThe 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 2y agonumber 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
- skybrian 2y agoDepends 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 2y agoJava does not have it yet. Project Valhalla might bring it with Value types.
- afiori 2y agoTypescript 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 2y agoNominal 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 2y agoYou can do classes, inheritance, and LSP with structural typing just fine; look at OCaml.
- jacobsimon 2y agoYou can still do this with classes in typescript: class Hash extends String {} https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoFMAeAXZA7AJjAyugE4CWWA5tAN4C+AUHcAPZYTrTnbJFibwRIAvNAAUZAA4BXdAC5obUhQCUc-kIB81OtGhFk6SUSzQsyAO5xEYrFPRKA3NAD0T6ADEmRaDmQBbFgq8JCwANNBg4uLYOGSUAEQIiMg4APpx0OhM0BLSdDT2DC7QAJI+YCAgAJ5hZsjQLFXQZmBY7JnQ4pAwiQLIMO3oCCQwAGaSWMDowViMAezMvp16atDCIj0IqohhObLyxLEq0ABGTEwgyC2rmlTauvqGxsSSyAX5ha4AoqhgixfQkggYE4s1Y7EB3GKNmkq2gcQgyGAenQABFeGA4gVmGDoBtYZxTDw+FYIUQobYHB9oAB1EiDJgw448XDJDKVKIQMKDbjIADkMCwWTIMWAQRYGUSbQQdWRj1ZCli2RgYFxiAAdHQiijkAA3ZAgJhRIgwXxgSooIhEFjIBkQRrES7sOnhZX3ciScBeBUUE6IsAQiU8-kmIW+XzJEi8OrYzAYcLodBgYDSnAZIXoMIXMAxH3tXzDCFhZBq8ga7EQc7Fg3kdbqzIAVUi3AAwpBkCIlJTNa4AHJMKbAOrcjpgHjhzDG8J6bLGMjMS2I9ieHxEUFsaCm9DJvqwhZLZBqESk8nSMIbBxAA https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
- brlewis 2y agoThat'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/GYVwdgxgLglg9mABOARgJwIZgCYFNsAiuAtnABQCUiA3gFCKIQA2GAzq4gIJgzEZM16DRDFYApDAHMQGNAC5EKOHCa4siALyJg-VrgDcQgL60hzNh04gocUihirBwkeKkz5i5avVadTPYYMJkKgkLAIiLhgkjBguADKcODYZBCyClY2dg64VHTOaLhQIGhIaWgAdKIS0rKIAPyIAEQAbmjKxE2ICk0ADtaI-VBNgYjBDBAIrCq4FUxwkmRRMXGJyWQALABMFBT6iAD0B4gAKgCevQkQaDC9UIy2vSyxrGZTM3MLS9GxCUk4ZDiAHcuDw+ExKLt9kdThcrjc7ohsHBcBwwHB7pNiE8MLFaCYgA https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABO... class Animal { isJaguar: boolean = false; } class Automobile { isJaguar: boolean = false; } function engineSound(car: Automobile) { return car.isJaguar ? "vroom" : "put put"; } console.log(engineSound(42)); // TypeScript complains console.log(engineSound(new Animal())); // TypeScript does not complain
- mason55 2y agoOr, 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/MYGwhgzhAEASkAtoFMAeAXZA7AJjAyugE4CWWA5tAN4C+AUKJDAKIC2YJIKG2e0hpCtXp0A9KOgBJHMjAgQATwA00AO7JoAeyyK1YLOmjpN0AA5NoCRMhjGjCEjABmAVyzB0JbQ20RDwTVZzImR4CCQAXmgACitwgC44RBUyUxd0RL9BcgBKRIAjTU0QWSxoCIA+ajpoaBD0FyIy4hdkAG46Gg6fLD9ocmxkIjBMNg4uKOjU9MziMlzEsc5yqqoauuQGpugsZFVoJZAprDT0HI6RcQPUMCCS6BcIMAGevsehyRP08ugAIghkMB6gAREZgX4dAK9QzIdjLKIDXbDUZwo7vIifU7nOhiCQAdQQRVMEBU6jMTGQOGg+hQqOpMDAlkQr0M7HQwAQNh+ASCYBCYQQ0Vh4xU6Mx6XOQA https://www.typescriptlang.org/play/?#code/MYGwhgzhAEASkAtoF...
- hn_throwaway_99 2y agoWhat you say is true, but after years of working with TypeScript (and about 15 years of Java before that) I'd say that from a purely practical perspective the structural typing approach is much more productive. I still have PTSD from the number of times I had to do big, painful refactorings in Java simply because of the way strong typing with nominal types works. I still shudder to think of Jersey 1.x to 2.x migrations from many years ago (and that was a PITA for many reasons beyond just nominal typing, but it could have been a lot easier with structural types). I love branded types (and what I think of their close cousin of string template literal types in TS) because they make code safer and much more self-documenting with minimal effort and 0 runtime overhead.
- RussianCow 2y agoI think proper type inference along with some sort of struct literal syntax could make a lot of these problems go away without the need for structural typing. If you can create structs without explicitly mentioning the type (as long as it can be inferred), then you can use the same syntax for different types as long as their signatures are the same. Structural typing makes it too easy to accidentally pass something that looks like a duck but is actually a goose. As an example, this syntax should be possible without structural typing or explicitly specifying the type of the value: // assuming a type signature of moveToPoint(Point) moveToPoint({x: 5, y: 10}) // the struct literal is inferred to be a Point I believe F# has syntax like this, but it's been a while since I've used it so I don't remember the details.
- hn_throwaway_99 2y ago> Structural typing makes it too easy to accidentally pass something that looks like a duck but is actually a goose. This may be true, but in reality after 7 years of using TypeScript I don't think I've ever encountered this as a bug for an object (Record) type. Even for branded types I find the value much more in self-documenting code than actual type safety, and I only have used branded types for primitives.
- sapling-ginger 2y agoAll valid javascript programs are valid typescript programs, so typescript necessarily must have structural type, it cannot have nominal type without breaking compatibility with javascript, which is a hard requirement.
- eru 2y agoBtw, there are quite a few language that have nominal typing, but use the equivalent of structural typing in their unions. Eg in Rust or Haskell you can distinguish `Option<Option<bool>>`, but not in these languages. I guess Python and Typescript are examples of these?