Skip to content

Type inference for generics not considering interfaces #8273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lazyoft opened this issue Apr 24, 2016 · 8 comments
Closed

Type inference for generics not considering interfaces #8273

lazyoft opened this issue Apr 24, 2016 · 8 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@lazyoft
Copy link

lazyoft commented Apr 24, 2016

TypeScript Version:
1.8.10
Code

interface IFooable<T> {}

class Foo {
    doStuff<T>(fooable: IFooable<T>, stuff: T) {
    }
}

class NumberFooable implements IFooable<number> {}
class StringFooable implements IFooable<string> {}

let foo = new Foo();
foo.doStuff(new NumberFooable(), ""); // was expecting a compilation error here
foo.doStuff(new StringFooable(), 42); // same here

Expected behavior:
Apparently when I create a class that implements an interface the system doesn't type check against the generic by using the interfaces, unless I declare it explicitly with the interface type.

let numberFooable: IFooable<number> = new NumberFooable();
foo.doStuff(numberFooable, ""); // gives a compilation error

Not sure if this is by design, but it looks quite odd to me.

@malibuzios
Copy link

malibuzios commented Apr 24, 2016

The problem, I believe, is that NumberFooable is assignable to StringFooable, and vice-versa:

interface IFooable<T> { }

class NumberFooable implements IFooable<number> { }
class StringFooable implements IFooable<string> { }

let x: NumberFooable = new StringFooable(); // No error
let y: StringFooable = new NumberFooable(); // No error

So here:

foo.doStuff(new NumberFooable(), ""); // was expecting a compilation error here

The type argument inference logic goes to the second argument and infers T to be string, it then checks if NumberFooable implements IFooable<string> and as you can see, it thinks that it does!

The reason for this is that IFooable<T> has an unreferenced generic variable. This means that having NumberFooable implement IFooable<number> does not exert any constraint on the structure of NumberFooable that includes a reference to T thus the generic argument annotation becomes meaningless.

Interestingly, even if the generic argument was applied to the class itself, it would still behave this way:

class Box<T> {}

let x: Box<number> = new Box<string>(); // No error
let y: Box<string> = new Box<number>(); // No error

I have spent an excessive amount of time analyzing and demonstrating more significant aspects of this problem and have even proposed a solution (see the examples I've given there, I believe they better illustrate the possible consequences of this phenomenon).

The team basically stamped it as "by design", which is sometimes their 'friendly' way of saying: "we know about this but we don't care, moreover, we consider it offensive to expose flaws in our original design: note, this forum is for bug reports, suggestions, and input that Microsoft likes and finds valuable, not a place to question our prior decisions and please stop wasting our precious time, we have better things to do" (and so do I..).

If that's what you intended when you asked whether this is by 'by design'? then based on their responses, I believe that would probably capture that well. That doesn't mean it's good design though, unless you're one of those people who sees it perfectly fine to have a Box<string> be assignable Box<number> in some cases, and would go for lengths to convince other people they need to go and study the 'canon' until they're brainwashed enough to believe it is the 'right' way and anyone else must be ignorant to see this as problematic.

@lazyoft
Copy link
Author

lazyoft commented Apr 24, 2016

Ok, I think I got it. Coming from C# this is quite confusing, but I can understand why in a dynamic language it makes more sense. I am still trying to wrap my head around the fact that types are structural and not nominal, but old habits continue to kick in. Hopefully they will introduce them someday. Going to close this issue for now, as by design.

@lazyoft lazyoft closed this as completed Apr 24, 2016
@malibuzios
Copy link

malibuzios commented Apr 24, 2016

Just wanted to mention the Javascript run-time has a nominal identification system for prototypes:

function A() { this.x = 1234 }
function B() { this.x = 1234 }

let a = new A();
let b = new B();

console.log(a instanceof A); // true
console.log(b instanceof B); // true
console.log(a instanceof B); // false, despite the compatible structure
console.log(b instanceof A); // false, despite the compatible structure

(same for ES6 classes)

So nominal typing is not necessarily a stranger to Javascript, at least when it comes to classes. To the contrary: TypeScript's type system does not actually successfully "capture" correct prototypical relations as it sees all classes as purely structural:

class A { x: number; }
class B { x: number; }

function giveMeA(arg: A) {
   if (!(arg instanceof A)) 
      throw new TypeError("That is not an A!");
}

giveMeA(new B()); // Runtime error, but no compilation error.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Apr 25, 2016

@malibuzios That's true. However, the interfaces and type parameters have no runtime representation in JavaScript. Furthermore, instanceof tests are brittle. Consider the following es5.1

var MyClass = (function () {
    function MyClass(value) {
        this.value = value;
    };
    return MyClass;
}());

function assertIsInstanceOf(instance, ctor) {
    if (!(instance instanceof ctor))
        throw new TypeError("instance is not an instance of " + ctor);
}

var myInstance = new MyClass();
assertIsInstanceOf(myInstance, MyClass); // OK

myInstance.__proto__ = window.Date;

assertIsInstanceOf(myInstance, MyClass); // Runtime error

This is not really nominal typing, it is an equality check on the prototype chain, based on comparison of properties of the objects themselves, what value their prototype refers to, which is pretty structural if you ask me.

EDIT: clarified example, remarks

@malibuzios
Copy link

@aluanhaddad

I was not referring to interfaces but particularly to classes which are strictly the case I was referring to on the other issues (in that sense I was digressing a bit).

The way TypeScript models ES6 classes today is only valid for unsafe Javascript code that does not actually apply run-time checks using instanceof, once these checks are introduced (either manually or through a tool), the pure structural approach for classes fails to model them correctly.

I was told that in a very early incarnation of the language, TypeScript did have nominal typing for classes but a structural one for interfaces, but the feedback they received was not very positive so they changed classes to become structural. They could have considered maintaining that functionality as an option, and I believe they may be reconsidering this now due to the community's feedback.

The case for allowing meaningless generic annotations when the generic variable is not referenced is a different one:

class Box<T> {}

let x: Box<number> = new Box<string>(); // No error
let y: Box<string> = new Box<number>(); // No error

It is unlikely, that anyone, say, in the history of the universe would actually find any remotely reasonable usage for this. Essentially the annotation here acts to purely confuse the programmer. I find it grossly disturbing from a design point of view, as it imprints a specialization on the type that is completely meaningless, but the only current way for the programmer to find this out is through gaining an understanding of this as an anomaly resulting from the particular way generics were implemented on top of structural typing at this language.

It is as if the language designers are saying: "yes we have a bug in our language but the only way for you to avoid it is to ask us or read about it in a forum" (this of course assumes you actually did notice it at all!). And then they say "oh, and you can have this checked through a linter", which is sometimes what they use as a "garbage bag" for things that may be important but they don't actually care about.

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Apr 25, 2016
@aluanhaddad
Copy link
Contributor

@malibuzios it doesn't change the reality that instanceof checks are brittle and that JavaScript does not have nominal typing. Even if you use some tool to enforce instanceof checks throughout your codebase, the language itself will not enforce it and a third party is free to create an object which masquerades as an instance of your class.

@malibuzios
Copy link

@aluanhaddad

Javascript doesn't have typing at all, and would rarely enforce it, even for primitives like number, string or boolean (e.g. even something as crazy as "abcd" * "dcba" just yields NaN and doesn't even error!). What I meant was that in order to model the run-time idioms for the how classes are usually (manually!) checked at run-time - which is not that different from the way number or string would be (manually!) checked at run-time, one would need to consider a system that somehow 'captures' the instanceof checks. That system would be close to (or some variation of) nominal typing.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants