I wonder why I can provide different generic interface in configure() method than in my class? In the first no error example, I provide IType<Args1> as a generic type for MyClass and then I can simply override it by IArgs2 that has a prop missing and I didn't get any error. Is it any way to ensure the types are exactly the same?
interface IArgs1 {
a: string;
b: string;
}
interface IArgs2 {
a: string;
}
interface IArgs3 {
d: string;
}
interface IType<T> {
configure(args: T): void
}
// no error - even if 'b' is missing from IArgs2
class Class implements IType<IArgs1> {
configure(args: IArgs2) {}
}
// error - because it's missing all IArgs1 attributes
class MyClass implements IType<IArgs1> {
configure(args: IArgs3) {}
}
CodePudding user response:
This is because T is in contravariant position.
Consider this example:
interface IArgs1 {
a: string;
b: string;
}
interface IArgs2 {
a: string;
}
type Covariance<T> = { box: T };
declare let args1: Covariance<IArgs1>;
declare let args2: Covariance<IArgs2>;
args1 = args2 // error
args2 = args1 // ok
As you might have noticed args2 is not assignable to args1. This is the opposite behavior.
Consider this example:
type Contravariance<T> = { box: (value: T) => void };
declare let args1: Contravariance<IArgs1>;
declare let args2: Contravariance<IArgs2>;
args1 = args2 // ok
args2 = args1 // error
Now, inheritance arrow has changed in opposite way. args1 is no more assignable to args2 whereas args2 is assignable to args1.
Same behavior you have in:
interface IType<T> {
configure(args: T): void
}
since IType<T> is the same as Contravariance in variance context.
This is why you don't have an error here:
// no error - even if 'b' is missing from IArgs2
class Class implements IType<IArgs1> {
configure(args: IArgs2) { }
}
because IArgs1 extends IArgs2
You have an error here:
// error - because it's missing all IArgs1 attributes
class MyClass implements IType<IArgs1> {
configure(args: IArgs3) {}
}
because IArgs1 and IArgs3 are completely different types without any relation.
Here you can find more about *-variance topic
CodePudding user response:
Looks like the class's intrinsic interface "comes first" and then all the implements constraints are checked against it afterward.
This means that, in the case of MyClass, the configure(args: IArgs1): void method signature (that originates from implements IType<IArgs1>) is checked against configure(args: IArgs3): void signature (that already exists in the class), Since IArgs3 requires IArgs1 to have the d: string property, this produces a conflict and a compiler error.
Conversely, in the case of Class, IArgs2 (that already exists in the class) requires IArgs1 (that comes from implements IType<IArgs1>) to have the a: string property, – which it has, and thus no errors are produced.
Not gonna lie, this is a bit of a surprise. I'd expect to first impose all implements A, B, C constraints on a class, and then fulfill them with actual class implementation, – but TypeScript isn't written by myself.
