Typed 'this' in object literal methods by ahejlsberg · Pull Request #14141 · microsoft/TypeScript (original) (raw)
With this PR we strongly type this
in methods of object literals and provide a facility for controlling the type of this
through contextual typing. The new behavior is only enabled in --noImplicitThis
mode.
The type of the expression this
in a method of an object literal is now determined as follows:
- If the method has an explicitly declared
this
parameter,this
has the type of that parameter. - Otherwise, if the method is contextually typed by a signature with a
this
parameter,this
has the type of that parameter. - Otherwise, if
--noImplicitThis
is enabled and the containing object literal has a contextual type that includes aThisType<T>
,this
has typeT
. - Otherwise, if
--noImplicitThis
is enabled and the containing object literal has a contextual type that doesn't include aThisType<T>
,this
has the contextual type. - Otherwise, if
--noImplicitThis
is enabledthis
has the type of the containing object literal. - Otherwise,
this
has typeany
.
Some examples:
// Compile with --noImplicitThis
type Point = { x: number; y: number; moveBy(dx: number, dy: number): void; }
let p: Point = { x: 10, y: 20, moveBy(dx, dy) { this.x += dx; // this has type Point this.y += dy; // this has type Point } }
let foo = { x: "hello", f(n: number) { this; // { x: string, f(n: number): void } }, }
let bar = { x: "hello", f(this: { message: string }) { this; // { message: string } }, }
In a similar manner, when --noImplicitThis
is enabled and a function expression is assigned to a target of the form obj.xxx
or obj[xxx]
, the contextual type for this
in the function expression is obj
:
// Compile with --noImplicitThis
obj.f = function(n) { return this.x - n; // 'this' has same type as 'obj' }
obj['f'] = function(n) { return this.x - n; // 'this' has same type as 'obj' }
In cases where an API produces a this
value by transforming its arguments, a new ThisType<T>
marker interface can be used to contextually indicate the transformed type. Specifically, when the contextual type for an object literal is ThisType<T>
or an intersection including ThisType<T>
, the type of this
within methods of the object literal is T
.
// Compile with --noImplicitThis
type ObjectDescriptor<D, M> = { data?: D; methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M }
function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M { let data: object = desc.data || {}; let methods: object = desc.methods || {}; return { ...data, ...methods } as D & M; }
let obj = makeObject({ data: { x: 0, y: 0 }, methods: { moveBy(dx: number, dy: number) { this.x += dx; // Strongly typed this this.y += dy; // Strongly typed this } } });
obj.x = 10; obj.y = 20; obj.moveBy(5, 5);
In the example above, the methods
object in the argument to makeObject
has a contextual type that includes ThisType<D & M>
and therefore the type of this
in methods within the methods
object is { x: number, y: number } & { moveBy(dx: number, dy: number): number }
. Notice how the type of the methods
property simultaneously is an inference target and a source for the this
type in methods.
The ThisType<T>
marker interface is simply an empty interface declared in lib.d.ts
. Beyond being recognized in the contextual type of an object literal, the interface acts like any empty interface.
Patterns similar to the above are used in several frameworks, including for example Vue and Ember. Using ThisType<T>
we can now more accurately describe those frameworks.
Supercedes #8382. We revoked that PR because it always made the type of this
in object literal methods be the type of the object literal. We now make that the default behavior, but allow the default to be overridden using a ThisType<T>
contextual type.