To get where structural type systems fall down, think about a bad case is when dealing with native state and you have a private long field with a pointer hiding in it used in native calls. Any “type” that provides that long will fit the type, leading to seg faults. A nominal type system allows you to make assurances behind the class name.
Anyways, this was a big deal in the late 90s, eg see opaque types https://en.wikipedia.org/wiki/Opaque_data_type.
class Foo {
public bar = 1;
private _value = 'hello';
static doSomething(f: Foo) {
console.log(f._value);
}
}
class MockFoo { public bar = 1; }
let mock = new MockFoo();
Foo.doSomething(mock); // Fails
Which is why you'd generally use interfaces, either declared or inline.In the pointer example, if the long field is private then it's not part of the public interface and you shouldn't run into that issue no?
You can do a lot just by hiding the private state and providing methods that operate on that private state in the type (using interfaces for example), but that approach doesn’t allow for binary methods (you need to reveal private state on a non-receiver in a method).
interface Foo {
fun doFoo()
}
You can call doFoo() on some value, and that value can refer to its private state that doesn’t appear in Foo.However, if you want to see the private data of an argument, private data has to appear in the signature (or use nominal typing). The easiest example is an equality method that compares private state.
interface Foo {
fun equals(otherFoo: Foo): Boolean
}
The receiver of the equals call can still refer to its private data, but whatever you a provided for otherFoo is only guaranteed to have the equals method. You might be able to deal with this isn’t an opaque type: interface FooModule {
export type t
fun equals(foo: t, otherFoo: t): boolean
makeFoo(): t
}
But you really aren’t doing structural typing anymore, and basically are using t like you would a name.