6 ms·
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 valu
by DanielHB 2y ago
I see where you are coming from but you are not quite understanding what the OP was saying
class A {
public value: number
}
class B {
public value: number
}
const x: A = new B() // no error
This is structural typing (shape defines type), if typescript had nominal typing (name defines type) this would give an error. You could brand these classes to forcefully cause this to error.
Branding makes structural typing work like nominal typing for the branded type only.
It is more like "doing what this article describes" is the default behaviour of most languages (most languages use nominal typing).
- 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
- dunham 2y ago> 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 2y agoYou 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 2y agoThere 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 2y agoThe 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 2y ago> 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 https://asana.com/inside-asana/typescript-quirks
- frenchy 2y ago> Branding makes structural typing work like nominal typing for the branded type only. That's not quite true. Branding doesn't exist at run time, where as nominal typing usually does at some level. Classes exist at runtime, but most typescript types don't, so unless there's something specific about the shape of the data that you can check with a type guard, it's impossible to narrow the type.
- deredede 2y ago> Classes exist at runtime Not necessarily, depending on the language. Functional languages and system languages such as OCaml, Haskell, Rust, but also C (painfully) and C++ can represent wrapper types within a nominal type system at no runtime cost.
- nequo 2y agoHaskell implements type classes via dictionary passing that don’t always get optimized away by the compiler so it does have a slight runtime cost: https://okmij.org/ftp/Computation/typeclass.html#dict https://okmij.org/ftp/Computation/typeclass.html#dict In Rust, using trait objects also generates a vtable for dynamic dispatch so in that case traits are not fully erased: https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/trait-objects.html https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/sh...
- deredede 2y agoMy quotation was not good - I intended to reply to the argument that "nominal type do [exist at runtime] to some level". The newtype pattern in either Haskell or Rust is fully transparent at runtime.
- garethrowlands 2y agoAs 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 2y agoGood article on using branded classes with Typescript to avoid structural typing: https://prosopo.io/articles/typescript-branding/ https://prosopo.io/articles/typescript-branding/ discussion: https://news.ycombinator.com/item?id=40146751 https://news.ycombinator.com/item?id=40146751