3

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) {}
}

1
  • configure(args: IArgs2) won't fail if you'll pass it object with additional properties (other than a), but configure(args: IArgs3) will fail if you won't pass it d Commented Nov 8, 2021 at 13:32

2 Answers 2

7

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

Sign up to request clarification or add additional context in comments.

Comments

2

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.