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.