Class type hints should act as interfaces

Topics: Language Specification
Oct 2, 2012 at 7:49 PM
Edited Oct 3, 2012 at 5:46 PM

If SomeOtherClass implements the interface which can be extracted by SomeClass, it should be usable in all places which expect SomeClass. For example the following should work: 

 

class Student {
    fullname : string;
    constructor(public firstname, public middleinitial, public lastname) {
        this.fullname = firstname + " " + middleinitial + " " + lastname;
    }
}

// The class definition also defines (implicitly) the interface version of this type:

//interface Student { 
//    firstname: string; 
//    lastname: string; 
//    middleinitial: string; 
//    fullname: string; 
//}


class MockStudent {
    fullname : string;
    mockdata: number;
    constructor(public firstname, public middleinitial, public lastname) {
        this.fullname = firstname + " " + middleinitial + " " + lastname;
        this.mockdata = 1;
    }
}


// In this context, "Student" is not a class but the implicitly defined interface:

function greeter(person:Student) {
    return "Hello, " + person.firstname + " " + person.lastname;
}

// As a result, this will work - because MockStudent implements everything that Student's implicit interface does.
greeter(new MockStudent("test", "x", "test")); 

// ... and without the interface boilerplate. 

 

Do we really want another language with elaborate and complex dependency injection frameworks?

If a function takes a db:MySQLDatabase argument, I really want it to also work with my mdb: MockDatabase implementation for my unit testing framework. Of course, that is provided that MockDatabase implements all members that MySQLDatabase implements.

Lets say that there is a cool chart drawing library class which is initialized with a canvas (this canvas becomes a private variable)

function init(canvas: Canvas) {
// stuff
}

Then time passes and someone writes a really fast webgl-based canvas. If TypeScript works as it does now, this old chart drawing library is useless. To make it useful, one would have to write a common interface ICanvas, then replace all occuranes of the "Canvas" type in the old library to be "ICanvas" (not to mention all other classes used internally by Canvas). Developers never seem to remember the need for this until its too late, and changing this is a nightmare.

If instead the type specifier means "any class that implements all the things implemented in Canvas", then this new webgl Canvas library would be easily usable with the old drawing library. Just initialize it with the new WebglCanvas which implements everything that Canvas implements. No changes required anywhere.

Is there *any* reason why things shouldn't work this way?

Coordinator
Oct 5, 2012 at 9:28 PM

Classes in TypeScript are structurally-typed.  They have a hidden member that allows them to be used nominally (meaning the way programmers of C# and other OO languages are accustomed).  To get something that is purely structural, you need to use interfaces.  We want to be able to tell whether the user is asking for the structural (interface) or a nominal (class) style. 

 

Oct 5, 2012 at 10:05 PM

We're discussing the same issue in the Class Brands and Type-Checking Weirdness thread. Please take a look at my proposal there which allows for both structural and nominal typing while making structural typing the default. Specifically, the proposal is that 'class MyClass' is conceptually or literally equivalent to defining 'interface MyClass' which contains the instance properties, but no hidden member for the brand AND 'interface MyClass$ extends MyClass { $MyClass$brand : string }'. In this way when you define a property or parameter of type 'MyClass', you mean 'something that looks like MyClass'  (structural) and when you define a property or parameter of type 'MyClass$', you mean something that is of type 'MyClass' or a subclass (nominal).