Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Those don't change that the type system is structural; the type system is actually not fully safe if you use the things you mention, eg:

  class Dog { woof(){} }
  class Cat { meow(){} }
  function f(a: Dog|Cat) {
    if (a instanceof Dog) { 
      a.woof()
    } else {
      a.meow()
    }
  }

  let dogish = {woof: ()=>{}}
  f(dogish)

This compiles because dogish is structurally a dog, the type system allows instanceof to narrow the type but "dogish instanceof Dog" is actually false, so at runtime this will crash after trying to call meow on dogish.


Yeah don't do that. :)

Do this:

  interface Dog { typeName: "Dog"; woof():void }
  interface Cat { typeName: "Cat"; meow():void }
  
  function isDog(a:Dog|Cat) : a is Dog {
    return a.typeName == "Dog"; // Some casting may be required here
  }
  
  function f(a: Dog|Cat) {
    if (isDog(a)) a.woof();
    else a.meow();
  }
  
  let dogish : Dog = {typeName:"Dog", woof: ()=>{ console.out("Woof this is Dog")}};
  f(dogish);
The neat thing about TypeScript's type system is that it's structural but you can pretty easily implement nominal types on top of it.

(And if you only need compile-time checks, you can make typeName nullable; {typeName?:"Dog"} != {typeName?:"Cat"})


Sure, but opt-in nominal types that you describe is still something of a foot-gun that you have to be wary of since its easy to accidentally pass something that conforms to the structural type but violates whatever expectation. Like here's an example:

  type SpecificJsonBlob = {x: number};
  function serialize(s: SpecificJsonBlob) { return JSON.stringify(s); }
  function addOneToX(s: SpecificJsonBlob) { s.x += 1 }
  [...]
  let o = { get x() { return 1 } }
  serialize(o)
  addOneToX(o)
This compiles because the getter makes it structurally conform to the type, but the 'serialize' will surprisingly return just '{}' since JSON.stringify doesn't care about getters, and addOneToX(o) is actually just a runtime crash since it doesn't implement set x. These are runtime issues that would be precluded by the type system in a normal structural typed language.

There's obviously other cases of ergonomic benefit to structural typing (including that idiomatic JS duck-typing patterns work as you'd hope), but I think its not unreasonable for someone to feel that it's their biggest problem with typescript (as grandparent OP did).


Idk, I think it's extremely statistically rare coincidence for nominative typing to be a better default than structural. In other words, it's much more useful (by default) to have some function work on more types than be a little bit more type-safe for the exactly the type author had in mind when writing it. In my opinion, type-safety has diminishing returns and the most of its usefulness lies in trivial things, like passing a string instead of some object or array just as a typo, or writing a wrong field name when mapping one object to another, but nominative typing lies way beyond my imaginary line marking the zone when types become more of annoyance than help.


This is pretty much what I ended up with (with a string const generic type param actually) but now I have this extra implementation detail that I need to add/remove at every service boundary. It just makes every serialization/deserialization more complicated and generally adds cruft.


The fact that it compiles is due to the TypeScript’s goal of being pragmatic rather than sound. If an instance isn’t derived from the class Dog, it doesn’t mean that it doesn’t conform to the type/interface Dog, yet TypeScript assumes it means that for the instance of check. It has more to do with TypeScript being a bolt-on over JavaScript that has to take into account existing codebases rather than with TypeScript being structurally typed.


My classic example case is actually even simpler IMHO:

    type userId = string;
    type subscriptionId = string;
    
    const uid: userId = 'userA';
    const sid: subscriptionId = uid; // compiler is OK with this


Isn't that just how type aliases work rather than anything to do with structural typing? That example also compiles in Haskell and Go and Kotlin.

Here's how type aliases are usually documented: "Type aliases do not introduce new types. They are equivalent to the corresponding underlying types."

The person above did have a better example of the downside: You usually want something to nominally comply with a specific interface, not structurally. i.e. Just because something happens to have an `encrypt(u8[]): u8[]` method doesn't mean you want to accept it as an argument. (Go interfaces have a similar issue.)


Those are just type aliases. They're just different names for `string`.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: