Reputation: 2065
Last week I asked a question about how to declare a class member that is a function whose parameter is covariant with the class, for which using the polymorphic this
type was a perfect solution.
My actual code has a class with a member which is a dictionary of such functions. In adapting the solution, ran in to an unexpected type error.
Given the following class declaration:
class C {
public dict1: {[x: string]: this} = {}; // Error: A 'this' type is available only in a non-static member of a class or interface.
public dict2: Record<string, this> = {}; // OK
public dict3: {[P in string]: this} = {}; // OK?!
}
Why does the declaration of dict1
, using an index signature, provoke the error
A 'this' type is available only in a non-static member of a class or interface
but the seemingly-equivalent declarations of dict2
(using the Record<>
utility type), and dict3
(using a different index signature based on the definition of Record<>) not provoke the same error?
Upvotes: 3
Views: 148
Reputation: 327859
See microsoft/TypeScript#47868 for a canonical answer to most of this question. The polymorphic this
type, as implemented in microsoft/TypeScript#4910, generally refers to the type of the this
value in the scope in which it is mentioned. For a type like
interface Foo { a: this }
this
refers to a Foo
or some subtype of Foo
, so you could implement it like
class Bar implements Foo { a = this; }
class Baz extends Bar { z = "abc" }
const baz = new Baz();
console.log(baz.a.a.a.z.toUpperCase()); // "ABC"
So baz
, baz.a
, baz.a.a
, etc. are all of type Baz
. That's great.
But for a type like
interface Qux { a: { b: this } } // error!
// -------------------> ~~~~
// only in an interface property or a class instance member
what does this
refer to? What's the relevant scope? One could possibly expect it to refer to the outer scope, and therefore it's Qux
(or a subtype), but maybe it refers to just the inner scope and thus it's Qux["a"]
(or a subtype). It's ambiguous.
TypeScript sidesteps the ambiguity by only allowing this
to be used in an interface property or a class instance member, with no intervening scopes. That's why the above is an error.
In order to specify which one you mean, you have to use some indirection, possibly with generics. If you want the outer scope, you can do this:
interface B<T> { b: T }
interface Qux { a: B<this> }
Now this
can only refer to a subtype of Qux
. You can implement it with a class like
class Quux implements Qux {
a = { b: this }
z = "abc";
}
const q = new Quux();
console.log(q.a.b.a.b.z.toUpperCase()) // "ABC"
So q
, q.a.b
, q.a.b.a.b
, etc, are all of type Quux
.
If you want the inner scope, you can do this:
interface B { b: this }
interface Qux { a: B }
Now this
can only refer to a subtype of B
. You can implement it like
class Quux implements Qux {
a = { get b() { return this }, z: "abc" };
}
const q = new Quux();
console.log(q.a.b.b.b.z.toUpperCase()) // "ABC"
Here I've used a getter but you could do it with another class if you want. Anyway now q.a
, q.a.b
, q.a.b.b
, etc, are of type Quux["a"]
.
For your example,
class C {
dict: { [x: string]: this } = {};
}
you've got an index signature, which behaves like a range of keys in an object type. An index signature can sit alongside other properties in an object type (e.g., {[k: string]: string; foo: "bar"}
).
And { [x: string]: this }
is just as ambiguous as { a: this }
. Does this
refer to an instance of C
or just to an the dict
property of C
? It looks like you intended the former. Thus we need some kind of indirection. You could do so like
type Dict<T> = { [x: string]: T };
class C {
dict: Dict<this> = {}; // okay
}
But of course you don't have to write your own Dict
type. The Record
utility type can be pressed into service this way, where Record<string, T>
is essentially the same thing:
class C {
dict: Record<string, this> = {}; // okay
}
And finally, if you define a mapped type inline like
class C {
dict: { [K in string]: this } = {}; // okay
}
it also works, because apparently a mapped type does not introduce its own scope. Note that while the syntax for mapped types looks similar to that of index signatures, they are not the same. (The question refers to this as an index signature, but it is not an index signature.) An index signature can be thought of a piece of an object type (e.g., other properties and signatures can appear inside an object type with it), while a mapped type is its own separate thing (e.g., the curly braces are part of the mapped type; you can't add other properties). See this answer to another question for more about that.
So there you go. You can only use polymorphic this
in a place where the scope is not ambiguous according to the compiler. You can't do it inside a nested object property, or an index signature, but you can do it with indirection, generics, and apparently mapped types.
Upvotes: 1