'TypeScript this in interface should reference itself and children, not only itself
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>
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.
Solution 1:[1]
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>
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
}
Solution 2:[2]
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();
}
}
Solution 3:[3]
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:
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).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | |
Solution 3 |