Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't typescript complain when I ignore generic type definitions?

Imagine you have the following code:

class Superclass<T>{
    combine(object: Superclass<T>) {
        console.log("combined!");
        return object;
    }
}

class Generic1 {
    x = 12;
    // some class...
}
class Generic2 {
    y = "Hello!";
    // some class...
}

new Superclass<Generic1>().combine(new Superclass<Generic2>());

Playground link

As you will surely notice, there should be an error here. Superclass<Generic1> can only be combined with Superclass<Generic1>, but in no case with Superclass<Generic2>. Visual studio code also displays this:

enter image description here

But there is no error after compiling! Why? Am I misunderstanding generics?

like image 743
MeineHTMLCodes Avatar asked Jul 28 '21 13:07

MeineHTMLCodes


1 Answers

TypeScript lets it go through because the T in Superclass<T> does not affect its shape, therefore Superclass<A> and Superclass<B> have the same shape, and are therefore considered "compatible" by TypeScript.

Remember that TypeScript has a structural type system where types are considered equivalent by their members, even if their actual and original definitions are completely separate (think of convergent evolution). As for why it's surprising: Remember that TypeScript does not have a nominative type-system, where types with identical members, structure and layout, right down to cross-platform binary representations, are considered strictly different, as long as they have different type-names (this is how Java, C#, and many other languages' type systems work).

So while Superclass<T> is not using T and it doesn't affect its shape, so tsc considers T superfluous and ignores it:

So this...

class Superclass<T> {
    combine(object: Superclass<T>): Superclass<T> { ... }
}

...is provably equivalent to:

class Superclass {
    combine(object: Superclass): Superclass { ... }
}

However, as soon as Superclass<T> becomes dependent upon T, such as by adding a field typed as T, then it throws an error:

class Superclass<T>{

    public value: T;

    constructor(value: T) {
        this.value = value;
    }

    combine(object: Superclass<T>) {
        console.log("combined: " + object.value.toString());
        return object;
    }
}

class Generic1 {
    x = Math.random();
}
class Generic2 {
    y = Math.random();
}

var x = new Superclass<Generic1>( new Generic1() ).combine(new Superclass<Generic2>( new Generic2() ) );

Error:

Argument of type 'Superclass<Generic2>' is not assignable to parameter of type 'Superclass<Generic1>'.
  Property 'x' is missing in type 'Generic2' but required in type 'Generic1'.

As for why this is the case - this is (I think) explained in TypeScript's documentation in the section about Type Compatibility:

The basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x

In this case, when Superclass<T> is closed as either Superclass<Generic1> or as Superclass<Generic2> both types have exactly the same members, and TypeScript considers them compatible wither each other.

like image 189
Dai Avatar answered Sep 24 '22 22:09

Dai