12

There is the example:

interface A {
    method: (itself: this) => string;
}

interface B extends A {
    additionalProperty: boolean;
}

type Extend<T extends A> = A & { extension: number };

type ExtendedA = Extend<A>

type ExtendedB = Extend<B>

The sandbox.

When I try to extend B TypeScript writes:

Type 'B' does not satisfy the constraint 'A'. Types of property 'method' are incompatible. Type '(itself: B) => string' is not assignable to type '(itself: A) => string'. Types of parameters 'itself' and 'itself' are incompatible. Property 'additionalProperty' is missing in type 'A' but required in type 'B'.(2344) input.tsx(6, 2): 'additionalProperty' is declared here.

But B extends A. They should be compatible.

UPDATE #1:

I can't explain this, but it seems that if I replace interfaces by classes typing works perfect.

UPDATE #2:

Well, it works only with class methods, but it doesn't work, for example, arrow functions. Still strange.

UPDATE #3

If the interface is defined in the following way it doesn't work:

interface A {
    method: (itself: this) => string;
}

But if it's defined in the following way it does work:

interface A {
    method(itself: this): string;
}

It has no sense at all. But looking for the reason of this behavior I found this excellent answer. It gave me a clue to reasons of this difference.

There was mentioned the TypeScript option strictFunctionTypes.

When enabled, this flag causes functions parameters to be checked more correctly.

During development of this feature, we discovered a large number of inherently unsafe class hierarchies, including some in the DOM. Because of this, the setting only applies to functions written in function syntax, not to those in method syntax

It explains the reason of that strange difference in behavior. I can just turn off this option, but it feels like a workaround.

I still need another solution.

UPDATE #4

I assume this error is designated to prevent such unsafe assignments:

const a: A = {
    method(b: B) {
        return `${b.toString()} / ${b.additionalProperty}`;
    }
}

But such type of errors are not specific for my case.

UPDATE #5

I've found another workaround

type Method<T extends (...args: any) => any> = {
    f(...args: Parameters<T>): ReturnType<T>;
}['f'];
interface A {
    method: Method<(itself: this) => string>;
}

interface B extends A {
    additionalProperty: boolean;
}

type Extend<T extends A> = T & { extension: number };

type ExtendedA = Extend<A>

type ExtendedB = Extend<B>

Check it yourself. It's better than disabling of strictFunctionTypes, but it's still workaround.

nitrovatter
  • 931
  • 4
  • 13
  • 30

3 Answers3

2

The this keyowrd is dependent on the context where it is being defined. When it is defined in interface A it references A, and in interface B it references B

Hence they both become incompatible when checking the extends constraint.

Solution 1 :

So either we can explicitly define them separately or just add a union of them A | B

interface A {
    method: (itself: A | B) => string;
}

interface B extends A {
    additionalProperty: boolean;
}

type Extend<T extends A> = T & { extension: number };

type ExtendedA = Extend<A>

type ExtendedB = Extend<B>

Code Playground

Solution 2 :

We can make method<T> (): void a generic function so that the context-related ambiguity for this is solved.

interface A {
    method: <T> (itself: T) => void;
}

interface B extends A {
    additionalProperty: boolean;
}

type Extend<T extends A> = T & { extension: number };

type ExtendedA = Extend<A>

type ExtendedB = Extend<B>

const b: B = {
    method<B>() {
        console.log(this.additionalProperty) 
    },
    additionalProperty: true
}

Code Playground Solution 2

Bishwajit jha
  • 384
  • 3
  • 9
  • I've tried it before. This approach has the disadvantage. There are the problems with the definition of the method type. It's only argument should have the type `B`, but it has the type `A | B` and it leads to headache (https://www.typescriptlang.org/play?ts=4.6.2#code/JYOwLgpgTgZghgYwgAgILIN4CgCQBbCMACwHsATALmQApgwBnCAGxivQB9kAhASmQF4AfMgBuJYGQDcWAL5YsoSLEQouyCAA9IIMvTSZccMmTrASIOEwAKUEgAdoYAJ5UARiRJMIcENLlZnB2QAUS0IHQAeABV1MJ09VGF+ZBiAMkxY7XozECoQAFc8V2hkGWkApyDQ7TIIMnRk6vCyCMT5QJQmnTq1RriWrkF5BHN6MGRXKl6DfEJSMmpXPmwcHBGQek8IADomEgBzRe2jEzAcyxt7Ryc+XBkAGkNjU3ML2wcoZyowKHyIWSwQA) – nitrovatter May 03 '22 at 07:48
  • @nitrovatter What if we solve the problem, by making method Generics `method` so that there will be no context-related ambiguity? [Code Playground](https://tsplay.dev/N7O94N) I will update my answer if it satisfies your requirement – Bishwajit jha May 03 '22 at 09:03
  • It's pretty annoying to define for each of generic functions. Also, you are able to set any possible type here. I've tried to solve it the following way, but get the strange error: https://www.typescriptlang.org/play?ts=4.6.2#code/JYOwLgpgTgZghgYwgAgILIN4CgCQBbCMACwHsATALmQB4AVZCAD0hDIGc1kBeZY4NgHzIAFMDBsIAGxhVaASm5CAbiWBkA3FgC+WLKEixEKAEINmEVh3TYccMmTHASIOJIAKUEgAdoYAJ5UAEYkJJIQcCCaOlgIzmxgyIFUpjw2BMTkouJSMAo2OGIS0gB0dg5gTi7unj5Q-rhaADS4ZY7Orh7evgG8UACuENq6-j7IAKLmrHRmLOxoQjz0AGSYMxZslVQgfXiB0MhamlgjKBOzEGToPGcWZNSoAsN+ozesFynjk3fGj0A – nitrovatter May 03 '22 at 09:38
  • Why are you trying to type `itself`, you don't need it actually for your purpose Just use `this` keyword. `const b: B = { method() { this.additionalProperty }, additionalProperty: true }` – Bishwajit jha May 03 '22 at 09:44
  • This functions can be stored and used separately. Sometimes `this` won't be defined. – nitrovatter May 03 '22 at 09:49
0

What if you add a type constraint on the method definition of your A interface?

interface A {
  method: <T extends this> (itself: T) => string;
}

Could you use a class instead?

abstract class A  {
    abstract method(itself: { [field in keyof this]: this[field] }): string;
}

interface B extends A {
    additionalProperty: boolean;
}

type Extend<T extends A> = T & { extension: number };

type ExtendedA = Extend<A>

type ExtendedB = Extend<B>

const t: ExtendedB = {
    additionalProperty: true,
    extension: 1,
    method(obj) {
        return obj.additionalProperty.toString();
    }
}

See it in action

Some random IT boy
  • 7,569
  • 2
  • 21
  • 47
  • Interesting idea. But it works strange: https://www.typescriptlang.org/play?ts=4.6.2#code/JYOwLgpgTgZghgYwgAgILIN4CgCQBbCMACwHsATALmQB4AVZCAD0hDIGdljg2A+ZACmBg2EADYwqtAJTIAvHzZgooAOYBuLAF8sWUJFiIUAIQbMIrDumw44ZMkOAkQcUQAUoJAA7QwATyoARiQkohBwIBraWH7eyACiZqx0pizsaHyyyPQAZJgp5myOIFQgAK54AdDImhrRvrEJqRBk6JmN5mTUqDw6MSjtrM0mbYmdRj1YCE6KnFQDZENymLi29mBFLu5ePv6cUKUQADS4TCyFTlQAjMf4hKRk-CQBAFYy1jhQhKVQIMhPzwA6VYOJybDzeKB+AFgEgAZSUqn4Ug0OG0miAA – nitrovatter May 12 '22 at 19:26
  • Could you use a class instead of an interface? – Some random IT boy May 12 '22 at 21:46
  • I can, but not in an every situation. I've explain the details in my answer above with hack solution: https://stackoverflow.com/a/72222665/10118104 – nitrovatter May 12 '22 at 22:37
0

Summary

This problem occurred only if strictFunctionTypes flag is on (that's default in the strict mode, so it's common case). Under this flag function type parameter positions are checked contravariantly instead of bivariantly.

In this case: (arg: B) => void is not assignable to (arg: A) => void even if B extends A,

Solutions:

a) Use methods instead of properties if it's possible. The methods type parameter positions are checked bivariantly.

b) Use bivariance hack to force TypeScript checks type parameter positions bivariantly.

type Bivariant<T extends (...args: any) => any> = {
    f(...args: Parameters<T>): ReturnType<T>;
}['f'];
let callback: Bivariant<(event: E) => void>

c) Disable strictFunctionTypes flag. But it won't help the other users that will use your code, so it's barely solution.

Root of the problem.

Again, this problem occurred only if strictFunctionTypes flag is on.

Under this flag function type parameter positions are checked contravariantly instead of bivariantly.

This strange words should not bother you. I won't dive into the types theory (but I recommend you to read this great answer).

Let's say B extends A, C extends B, D extends C and so on. Then:

Scheme

In the question we have the problem assigning B to A cause its methods are not comparable. If C extends B, then if parameter positions are checked contravariantly method(B) is not comparable to method(C) (look at the image).

Why do I need strictFunctionTypes in a first place?

Let's look at the motivation example. First of all, disable the strictFunctionTypes flag.

interface Article {
  content: string;
}

interface TitledArticle extends Article {
  title: string;
}

let printer: (article: Article) => string;

Let's create the printer for the titled article:

let printer = (article: TitledArticle) => {
   return `${article.title.toUpperCase()} / ${article.content}`
}

printer({ content: "Great Answer" }); // exception

We've got exception, but TypeScript didn't warn us!

Well, it will warn if you enable strictFunctionTypes. That's the point.

Once again: it's danger to assign the function (arg: B) => void to (arg: A) => void, because after such assignment you still will be able to pass A to this function and it can lead to different troubles, because A doesn't have all properties of B. That's why this rule is needed.

Why do I need check function type parameter positions bivariantly ever?

Let's create the custom CustomArray:

class CustomArray<T> {
    // it's not method, it's a property and it's important!
    push = (item: T) => {
        // something
    }
}

let a = new CustomArray<number>();

let b = new CustomArray<0>();

a = b; // typescript error
Type 'CustomArray<0>' is not assignable to type 'CustomArray<number>'.
  Type 'number' is not assignable to type '0'.

The same problem I assume occurred with default Array. That's why the Typescript developers leaves bivariantly checking of function type parameter position for constructors and methods, even if strictFunctionTypes is on.

During development of this feature, we discovered a large number of inherently unsafe class hierarchies, including some in the DOM. Because of this, the setting only applies to functions written in function syntax, not to those in method syntax

You can check it himself, by replacing the property push by a method:

class CustomArray<T> {
    push(item: T) {
        // something
    }
}

let a = new CustomArray<number>();

let b = new CustomArray<0>();

a = b; // no error

So, you can rely on this peculiarity and use methods. But there are situations, where methods are just not good enough for you.

There are examples of the types where you can't use methods

interface A {
  method?: (a: this) => void;
}
interface A {
  method: ((a: this) => number) | number;
}
const callback = (e: Event) => void;

What can you do do in that situations?

Bivariance hack

There is the trick that can help you to force TypeScript check function type parameter positions bivariantly.

The idea is simple: if strictFunctionTypes doesn't applies to methods, let's make the function a method.

let callback: (event: E) => void
let callback: {'method': (event: E) => void}['method']

It's so easy but it works!

Let's write the generic type:

type Bivariant<T extends (...args: any) => any> = {
    f(...args: Parameters<T>): ReturnType<T>;
}['f'];
let callback: Bivariant<(event: E) => void>

It's still hack, but it's even nice now.

If you use it, don't worry. You are not alone. React, Material UI and many others use it. You are in the good company, just use it wisely, because with great power comes great responsibility (well, in a boring style, it reduces types safety).

nitrovatter
  • 931
  • 4
  • 13
  • 30