Considering an interface IDog
with the method likes<T extends IDog>( other: T )
. The method takes an argument whose type extends the interface. Why is not allowed to implement that method in a derived class Dog
using the class as type of the argument instead of the interface?
interface IDog
{
likes<T extends IDog>( other: T ): boolean;
}
class Dog implements IDog
{
private name = "Buddy";
public likes<T extends Dog>( other: T )
// ^^^^^
// error: Property 'likes' in type 'Dog' is not
// assignable to the same property in base type 'IDog' [...]
// Property 'name' is missing in type 'IDog' but required in type 'Dog'
{
return true;
}
}
Removing the private property name
would make the error go away but is not a solution for my real world problem. The weird thing is though, that the same example without generics works just fine:
interface ICat
{
likes( other: ICat ): boolean;
}
class Cat implements ICat
{
private name = "Simba";
public likes( other: Cat ) // no error using Cat here (instead of ICat)
{
return true;
}
}
What am I missing here?
Only generic classes can implement generic interfaces. Normal classes can't implement generic interfaces.
You can declare variant generic interfaces by using the in and out keywords for generic type parameters. ref , in , and out parameters in C# cannot be variant. Value types also do not support variance. You can declare a generic type parameter covariant by using the out keyword.
The short answer is, that there is no way to find out the runtime type of generic type parameters in Java. A solution to this is to pass the Class of the type parameter into the constructor of the generic type, e.g.
Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.
Let's imagine that the compiler had no problem with the way you are implementing IDog
. Then the following would be fine:
class Dog implements IDog {
private name = "Buddy";
public likes<T extends Dog>(other: T) {
return other.name.toUpperCase() === "FRIEND";
}
}
const myDog: IDog = new Dog(); // should be okay if Dog implements IDog
But that can lead to runtime errors that would not be caught by the compiler:
const eyeDog: IDog = {
likes(other) {
return true;
}
}
console.log(myDog.likes(eyeDog)) // okay for the compiler, but RUNTIME ERROR
So the compiler is right that Dog
does not properly implement IDog
. Allowing this would be "unsound". If you have a function type you want to extend (make more specific), you cannot make its parameters more specific and be sound; you need to make them more general. This means that function parameters should be checked contravariantly (that is, they vary the opposite way from the function type... they counter-vary... contravariant).
Of course that leads to your question about Cat
. Doesn't the exact same argument work there?
class Cat implements ICat {
private name = "Simba";
public likes(other: Cat) { // no error
return other.name.toUpperCase() === "FRIEND";
}
}
const myCat: ICat = new Cat(); // no error
const eyeCat: ICat = {
likes(other) { return true; }
}
console.log(myCat.likes(eyeCat)) // no compiler error, but RUNTIME ERROR
Indeed it does! The compiler is allowing the unsound extension of ICat
with Cat
. What gives?
This is explicitly intentional behavior; method parameters are checked bivariantly, meaning the compiler will accept both wider parameter types (safe) and narrower parameter types (unsafe). This is apparently because, in practice, people rarely write the sort of unsafe code above with myCat
(or myDog
for that matter), and such unsafeness is what allows a lot of useful type hierarchies to exist (e.g., TypeScript allows Array<string>
to be a subtype of Array<string | number>
).
So, wait, why does the compiler care about soundness with generic type parameters but not with method parameters? Good question; I don't know that there's any "official" answer to this (although I might have a look through GitHub issues to see if someone in the TS team has ever commented on that). In general, the soundness violations in TypeScript were considered carefully based on heuristics and real-world code.
My guess is that people usually want type safety with their generics (as evidenced by microsoft/TypeScript#16368's implementation of stricter checks for them), and specifically adding extra code to allow method parameter bivariance would be more trouble than it's worth.
You can disable the strictness check for generics by enabling the --noStrictGenericChecks
compiler option, but I wouldn't recommend intentionally making the compiler less type safe, since it will affect much more than your Dog
issue, and it's hard to find resources for help when you rely on unusual compiler flags.
Note that you may be looking for the pattern where each subclass or implementing class can only likes()
parameters of its own type and not every possible subtype. If so, then you might consider using the polymorphic this
type instead. When you use this
as a type, it's like a generic type that means "whatever type the subclass calling this method is". But it's specifically made to allow the kind of thing you seem to be doing:
interface IGoldfish {
likes(other: this): boolean;
}
class Goldfish implements IGoldfish {
private name = "Bubbles";
public likes(other: this) {
return other.name.toUpperCase() === "FRIEND";
}
}
const myFish: IGoldfish = new Goldfish();
This, of course, has the same problem as the other two examples:
const eyeFish: IGoldfish = { likes(other) { return true; } }
console.log(myFish.likes(eyeFish)) // RUNTIME ERROR
so it's not a panacea for unsoundness. But it is very similar to the generic version without the generic parameter warning.
Playground link to code
Imagine such a situation: you have
const myDog: Dog
const someOtherDog: IDog
and such a function:
function seeIfLikes(dog: IDog, anotherDog: IDog) {
return dog.likes(anotherDog)
}
This function seems OK, IDog.likes()
wants something that extends IDog
as the argument.
But when you call seeIfLikes(myDog, someOtherDog)
, something unexpected happens: myDog
is casted to an IDog
, so TypeScript will forget that its likes()
method requires something that extends Dog
, not IDog
!
So this function call will pass the type checking even if someOtherDog
does not actually extend Dog
- and if your Dog.likes()
contains some code specific to the Dog
class, not to IDog
, you get a runtime kaboom.
This is why we cannot add new generic parameter restriction in subtypes: they may be casted to their supertypes, and that restriction will be gone. Hope this is clear enough to understand.
Yes, that Cat
example will suffer from exactly the same problem, but tsc let it pass for unknown reason. Maybe it is a limitation of the type system, or a bug that is better reported.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With