Typed 'this' in object literal methods #14141

Merged
merged 28 commits into from Mar 6, 2017

Conversation

Projects
None yet
8 participants
@ahejlsberg
Member

ahejlsberg commented Feb 17, 2017

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 a ThisType<T>, this has type T.
  • Otherwise, if --noImplicitThis is enabled and the containing object literal has a contextual type that doesn't include a ThisType<T>, this has the contextual type.
  • Otherwise, if --noImplicitThis is enabled this has the type of the containing object literal.
  • Otherwise, this has type any.

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.

@ahejlsberg ahejlsberg changed the title from Typed 'this to Typed 'this' in object literal methods Feb 17, 2017

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 17, 2017

Member

Latest commit adds contextual this type in assignments of the form obj.xxx = function(...) and obj[xxx] = function(...).

Member

ahejlsberg commented Feb 17, 2017

Latest commit adds contextual this type in assignments of the form obj.xxx = function(...) and obj[xxx] = function(...).

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 17, 2017

Member

The one issue that remains for discussion here is whether the strongly typed this behavior should always be in effect or only be in effect in --noImplicitThis mode. It is technically a breaking change, but it also provides a better authoring experience (statement completion for this) and catches legitimate bugs.

Member

ahejlsberg commented Feb 17, 2017

The one issue that remains for discussion here is whether the strongly typed this behavior should always be in effect or only be in effect in --noImplicitThis mode. It is technically a breaking change, but it also provides a better authoring experience (statement completion for this) and catches legitimate bugs.

@sandersn

sandersn requested changes Feb 17, 2017 edited

I like this change.

  1. Still need tests
  2. @bradenhs points out that the contextual typing code only applies to functions and object literal methods. It should apply to object literal getters/setters as well.
  3. I ran RWC to see what breaks. I found several good breaks, a few breaks that require a d.ts update with ThisType (Object.defineProperty, knockout and react), and a couple of places that show that this needs to be the intersection of the object literal type and the parameter type.
src/compiler/checker.ts
+ }
+
+ function getThisTypeFromContextualType(type: Type): Type {
+ return applyToContextualType(type, t => {

This comment has been minimized.

@sandersn

sandersn Feb 17, 2017

Member

shouldn't applyToContextualType just be named mapType and replace mapType? It doesn't do anything specific to contextual types that I could see, and it's better than the current mapType because it skips return values of undefined. #Resolved

@sandersn

sandersn Feb 17, 2017

Member

shouldn't applyToContextualType just be named mapType and replace mapType? It doesn't do anything specific to contextual types that I could see, and it's better than the current mapType because it skips return values of undefined. #Resolved

This comment has been minimized.

@ahejlsberg

ahejlsberg Feb 25, 2017

Member

Yes, that makes sense.

@ahejlsberg

ahejlsberg Feb 25, 2017

Member

Yes, that makes sense.

@@ -41,19 +41,51 @@ tests/cases/conformance/fixSignatureCaching.ts(639,38): error TS2304: Cannot fin
tests/cases/conformance/fixSignatureCaching.ts(640,13): error TS2304: Cannot find name 'window'.
tests/cases/conformance/fixSignatureCaching.ts(641,13): error TS2304: Cannot find name 'window'.
tests/cases/conformance/fixSignatureCaching.ts(704,18): error TS2339: Property 'prepareDetectionCache' does not exist on type '{}'.
+tests/cases/conformance/fixSignatureCaching.ts(704,45): error TS2339: Property '_cache' does not exist on type '{ constructor: (userAgent: any, maxPhoneWidth: any) => void; mobile: () => any; phone: () => any; tablet: () => any; userAgent: () => any; userAgents: () => any; os: () => any; version: (key: any) => any; versionStr: (key: any) => any; is: (key: any) => any; match: (pattern: any) => any; isPhoneSized: (maxPhoneWidth: any) => any; mobileGrade: () => any; }'.

This comment has been minimized.

@sandersn

sandersn Feb 17, 2017

Member

The purpose of this test isn't obvious without following the pointer back to issue #10697. Can you add one more comment at the top of the file like "this test originally failed by using 100% CPU and should now take less than 1 second to run". I'd like a faster way to know that these additional failures are not a problem.

@sandersn

sandersn Feb 17, 2017

Member

The purpose of this test isn't obvious without following the pointer back to issue #10697. Can you add one more comment at the top of the file like "this test originally failed by using 100% CPU and should now take less than 1 second to run". I'd like a faster way to know that these additional failures are not a problem.

this.mine
- this.willDestroy
+ //this.willDestroy

This comment has been minimized.

@sandersn

sandersn Feb 17, 2017

Member

just delete this, I think. #Resolved

@sandersn

sandersn Feb 17, 2017

Member

just delete this, I think. #Resolved

This comment has been minimized.

@ahejlsberg

ahejlsberg Feb 25, 2017

Member

Done.

@andy-ms andy-ms referenced this pull request in Microsoft/TSJS-lib-generator Feb 17, 2017

Open

HTMLElement onchange should use `this: this` #195

@sandersn

This comment has been minimized.

Show comment
Hide comment
@sandersn

sandersn Feb 17, 2017

Member

Summary of RWC breaks [1]: two good breaks from contextual typing of function assignment. Two breaks that show that Object.defineProperty and friends need to include ThisType<T>. One break that shows that choosing the contextual type of the object literal instead of the contextual type from a function call argument is incorrect.

[1] Note that I haven't looked at all of RWC yet; it hit a stack overflow halfway through.

  1. A good break resulting from contextual typing of function assignment.
declare function f(elt: HTMLElement): void;
function outer() {
  var self = this;
  var args: Draggable;
  args.helper = function () {
    return f(this); 
    
      // error 'this: Draggable' is not assignable to 'HTMLElement'
      //   Property 'accessKey' is missing in 'Draggable'
  };
}

This catches code that is just wrong. I looked at Draggable and it's not at all related to HTMLElement.

(This happened in two different projects.)

  1. Another break from contextual typing of function assignment.
// Override the prototype method on C
CBase.prototype.debug = function() {
  var self: C = this;
    // error type 'CBase' is not assignable to 'C'
    //   Property '_subclassProperty' is missing in 'CBase'

I'm not sure what to make of this. The comment makes it sound like the intention is to override the method on C, not on CBase. So probably this is a good error too.

  1. Defining properties on Array.prototype using Object.defineProperties.
Object.defineProperties(Array.prototype, {
  empty: {
    value: function() {
      return this.length === 0;
    }
  }
})

It looks like Object.defineProperties and related methods need to use ThisType to provide the correct type for this here.

  1. Implicit any errors on a this type that has no index signature.
function defineProperty(c: any, name: string, defaultValue: any) {
  var backingFieldName = "_" + name;
  Object.defineProperty(
    c.prototype,
    name,
    {
      get: function(): any {
        if (typeof this[backingFieldName] === 'undefined') {
          this[backingFieldName] = defaultValue;
        }
        return this[backingFieldName];
      }, // code continues ...
    }); 
};

This would also be fixed by adding ThisType to Object.defineProperty because c.prototype: any and would avoid the noImplicitAny error.

  1. Object literal type overrides contextual type:
interface MockJaxOptions {
  url?: string;
  responseTime?: number;
  status?: number;
  responseText?: string;
  responseHeaders: StringMap<string>;
}

$.mockjax({
  type: "GET",
  url: "blah/blah/blah",
  status: 200,
  contentType: "application/json",
  response: function() {
    this.responseText = JSON.stringify({ stuff: "things" });
    // error: property 'responseText' does not exist on '{ type: string ... }'
  }
});

This is bad because $.mockjax shouldn't really need to have its parameter be of type MockJaxOptions & ThisType<MockJaxOptions>.
I have a hunch this is fairly old code because JQuery MockJax now has its own d.ts. So we could fix this particular break in only one place, but it is symptomatic of a larger problem.

Member

sandersn commented Feb 17, 2017

Summary of RWC breaks [1]: two good breaks from contextual typing of function assignment. Two breaks that show that Object.defineProperty and friends need to include ThisType<T>. One break that shows that choosing the contextual type of the object literal instead of the contextual type from a function call argument is incorrect.

[1] Note that I haven't looked at all of RWC yet; it hit a stack overflow halfway through.

  1. A good break resulting from contextual typing of function assignment.
declare function f(elt: HTMLElement): void;
function outer() {
  var self = this;
  var args: Draggable;
  args.helper = function () {
    return f(this); 
    
      // error 'this: Draggable' is not assignable to 'HTMLElement'
      //   Property 'accessKey' is missing in 'Draggable'
  };
}

This catches code that is just wrong. I looked at Draggable and it's not at all related to HTMLElement.

(This happened in two different projects.)

  1. Another break from contextual typing of function assignment.
// Override the prototype method on C
CBase.prototype.debug = function() {
  var self: C = this;
    // error type 'CBase' is not assignable to 'C'
    //   Property '_subclassProperty' is missing in 'CBase'

I'm not sure what to make of this. The comment makes it sound like the intention is to override the method on C, not on CBase. So probably this is a good error too.

  1. Defining properties on Array.prototype using Object.defineProperties.
Object.defineProperties(Array.prototype, {
  empty: {
    value: function() {
      return this.length === 0;
    }
  }
})

It looks like Object.defineProperties and related methods need to use ThisType to provide the correct type for this here.

  1. Implicit any errors on a this type that has no index signature.
function defineProperty(c: any, name: string, defaultValue: any) {
  var backingFieldName = "_" + name;
  Object.defineProperty(
    c.prototype,
    name,
    {
      get: function(): any {
        if (typeof this[backingFieldName] === 'undefined') {
          this[backingFieldName] = defaultValue;
        }
        return this[backingFieldName];
      }, // code continues ...
    }); 
};

This would also be fixed by adding ThisType to Object.defineProperty because c.prototype: any and would avoid the noImplicitAny error.

  1. Object literal type overrides contextual type:
interface MockJaxOptions {
  url?: string;
  responseTime?: number;
  status?: number;
  responseText?: string;
  responseHeaders: StringMap<string>;
}

$.mockjax({
  type: "GET",
  url: "blah/blah/blah",
  status: 200,
  contentType: "application/json",
  response: function() {
    this.responseText = JSON.stringify({ stuff: "things" });
    // error: property 'responseText' does not exist on '{ type: string ... }'
  }
});

This is bad because $.mockjax shouldn't really need to have its parameter be of type MockJaxOptions & ThisType<MockJaxOptions>.
I have a hunch this is fairly old code because JQuery MockJax now has its own d.ts. So we could fix this particular break in only one place, but it is symptomatic of a larger problem.

@sandersn

This comment has been minimized.

Show comment
Hide comment
@sandersn

sandersn Feb 17, 2017

Member

More breaks:

  1. In vscode/src/vs/languages/markdown/common/markdownWorker.ts:
    	public getEmitOutput(resource: URI, absoluteWorkersResourcePath: string): WinJS.TPromise<Modes.IEmitOutput> { // TODO@Ben technical debt: worker cannot resolve paths absolute
    		let model = this.resourceService.get(resource);
    		let cssLinks: string[] = this.cssLinks || [];
    
    		// Custom Renderer to fix href in images
    		let renderer = new Marked.marked.Renderer();
    		let $this = this;
    		renderer.image = function(href: string, title: string, text: string): string {
    			let out = '<img src="' + $this.fixHref(resource, href) + '" alt="' + text + '"';
    			if (title) {
    				out += ' title="' + title + '"';
    			}
    
    			out += (this.options && this.options.xhtml) ? '/>' : '>';
    			             ~~~~~~~
!!! error TS2339: Property 'options' does not exist on type 'MarkedRenderer'.
    			                             ~~~~~~~
!!! error TS2339: Property 'options' does not exist on type 'MarkedRenderer'.

This seems like a good break. I can't find a property named 'options' in MarkedRenderer or in the class that contains getEmitOutput.

  1. Knockout's observable.fn entries don't specify that this is callable.
ko.observable.fn['toInt'] = function() {
  return parseInt(this());
                  ~~~~~~
!!! error TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'KnockoutObservableFunctions<any>' has no compatible call signatures.
}

This seems like an easy fix to knockout's d.ts. It's just that nobody ever noticed that KnockoutBindingHandler needed to have a line (): any at the beginning.

  1. RxJs/src/observable/dom/AjaxObservable.ts gets contextual typing from assignment
      private setupEvents(xhr: XMLHttpRequest, request: AjaxRequest) {
        const progressSubscriber = request.progressSubscriber;
    
        xhr.ontimeout = function xhrTimeout(e) {
          const {subscriber, progressSubscriber, request } = (<any>xhrTimeout);
          if (progressSubscriber) {
            progressSubscriber.error(e);
          }
          subscriber.error(new AjaxTimeoutError(this, request)); //TODO: Make betterer.
                                                ~~~~
!!! error TS2345: Argument of type 'XMLHttpRequestEventTarget' is not assignable to parameter of type 'XMLHttpRequest'.
!!! error TS2345:   Property 'onreadystatechange' is missing in type 'XMLHttpRequestEventTarget'.
        };

This seems like a good break too. The comment even indicates that the author is suspicious that something is wrong.

  1. Angular 2 decorators_spec.ts
export function main() {
      describe('es5 decorators', () => {
        it('should declare directive class', () => {
          var MyDirective = Directive({}).Class({constructor: function() { this.works = true; }});
                                                                                ~~~~~
!!! error TS2339: Property 'works' does not exist on type '{ constructor: () => void; }'.
          expect(new MyDirective().works).toEqual(true);
        });

This is probably another case where the type of the object literal needs to be intersected with the contextual type from the call signature. But it's in a test. It could just be a completely dynamic use of an undeclared property.

  1. Angular 2 React benchmark
    // tree benchmark in React
    import {getIntParameter, bindAction} from 'angular2/src/test_lib/benchmark_util';
    import * as React from './react.min';
    
    var TreeComponent = React.createClass({
      displayName: 'TreeComponent',
    
      render: function() {
        var treeNode = this.props.treeNode;
                            ~~~~~
!!! error TS2339: Property 'props' does not exist on type '{ displayName: string; render: () => any; }'.

This is evidence that React.createClass will need to update its d.ts too.

Member

sandersn commented Feb 17, 2017

More breaks:

  1. In vscode/src/vs/languages/markdown/common/markdownWorker.ts:
    	public getEmitOutput(resource: URI, absoluteWorkersResourcePath: string): WinJS.TPromise<Modes.IEmitOutput> { // TODO@Ben technical debt: worker cannot resolve paths absolute
    		let model = this.resourceService.get(resource);
    		let cssLinks: string[] = this.cssLinks || [];
    
    		// Custom Renderer to fix href in images
    		let renderer = new Marked.marked.Renderer();
    		let $this = this;
    		renderer.image = function(href: string, title: string, text: string): string {
    			let out = '<img src="' + $this.fixHref(resource, href) + '" alt="' + text + '"';
    			if (title) {
    				out += ' title="' + title + '"';
    			}
    
    			out += (this.options && this.options.xhtml) ? '/>' : '>';
    			             ~~~~~~~
!!! error TS2339: Property 'options' does not exist on type 'MarkedRenderer'.
    			                             ~~~~~~~
!!! error TS2339: Property 'options' does not exist on type 'MarkedRenderer'.

This seems like a good break. I can't find a property named 'options' in MarkedRenderer or in the class that contains getEmitOutput.

  1. Knockout's observable.fn entries don't specify that this is callable.
ko.observable.fn['toInt'] = function() {
  return parseInt(this());
                  ~~~~~~
!!! error TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'KnockoutObservableFunctions<any>' has no compatible call signatures.
}

This seems like an easy fix to knockout's d.ts. It's just that nobody ever noticed that KnockoutBindingHandler needed to have a line (): any at the beginning.

  1. RxJs/src/observable/dom/AjaxObservable.ts gets contextual typing from assignment
      private setupEvents(xhr: XMLHttpRequest, request: AjaxRequest) {
        const progressSubscriber = request.progressSubscriber;
    
        xhr.ontimeout = function xhrTimeout(e) {
          const {subscriber, progressSubscriber, request } = (<any>xhrTimeout);
          if (progressSubscriber) {
            progressSubscriber.error(e);
          }
          subscriber.error(new AjaxTimeoutError(this, request)); //TODO: Make betterer.
                                                ~~~~
!!! error TS2345: Argument of type 'XMLHttpRequestEventTarget' is not assignable to parameter of type 'XMLHttpRequest'.
!!! error TS2345:   Property 'onreadystatechange' is missing in type 'XMLHttpRequestEventTarget'.
        };

This seems like a good break too. The comment even indicates that the author is suspicious that something is wrong.

  1. Angular 2 decorators_spec.ts
export function main() {
      describe('es5 decorators', () => {
        it('should declare directive class', () => {
          var MyDirective = Directive({}).Class({constructor: function() { this.works = true; }});
                                                                                ~~~~~
!!! error TS2339: Property 'works' does not exist on type '{ constructor: () => void; }'.
          expect(new MyDirective().works).toEqual(true);
        });

This is probably another case where the type of the object literal needs to be intersected with the contextual type from the call signature. But it's in a test. It could just be a completely dynamic use of an undeclared property.

  1. Angular 2 React benchmark
    // tree benchmark in React
    import {getIntParameter, bindAction} from 'angular2/src/test_lib/benchmark_util';
    import * as React from './react.min';
    
    var TreeComponent = React.createClass({
      displayName: 'TreeComponent',
    
      render: function() {
        var treeNode = this.props.treeNode;
                            ~~~~~
!!! error TS2339: Property 'props' does not exist on type '{ displayName: string; render: () => any; }'.

This is evidence that React.createClass will need to update its d.ts too.

@bradenhs bradenhs referenced this pull request in mobxjs/mobx-state-tree Feb 21, 2017

Closed

Typescript Examples #9

@bradenhs

This comment has been minimized.

Show comment
Hide comment
@bradenhs

bradenhs Feb 21, 2017

I just tried this out and thought this would work:

const person = {
  firstName: 'First',
  lastName: 'Last',
  getName() {
    // works!
    return this.firstName + ' ' + this.lastName;
  },
  get name() {
    // 'this' implicitly has type 'any' because it does not have a type annotation.
    return this.firstName + ' ' + this.lastName;
  },
};

But it looks like the getter isn't aware of the type of this. Is this behavior correct?

bradenhs commented Feb 21, 2017

I just tried this out and thought this would work:

const person = {
  firstName: 'First',
  lastName: 'Last',
  getName() {
    // works!
    return this.firstName + ' ' + this.lastName;
  },
  get name() {
    // 'this' implicitly has type 'any' because it does not have a type annotation.
    return this.firstName + ' ' + this.lastName;
  },
};

But it looks like the getter isn't aware of the type of this. Is this behavior correct?

@sandersn

This comment has been minimized.

Show comment
Hide comment
@sandersn

sandersn Feb 21, 2017

Member

@bradenhs Nope, that is not correct. But the code right now specifically works with the contextual type of this in functions and methods. It needs code to support getters and setters too. I'll update my review comment above so @ahejlsberg will see it when he gets back from vacaation.

Member

sandersn commented Feb 21, 2017

@bradenhs Nope, that is not correct. But the code right now specifically works with the contextual type of this in functions and methods. It needs code to support getters and setters too. I'll update my review comment above so @ahejlsberg will see it when he gets back from vacaation.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 23, 2017

Member

With latest commits we now properly type this in getters and setters.

Member

ahejlsberg commented Feb 23, 2017

With latest commits we now properly type this in getters and setters.

@@ -22,9 +21,6 @@ tests/cases/conformance/types/thisType/thisTypeInAccessorsNegative.ts(16,22): er
}
const contextual: Foo = {

This comment has been minimized.

@sandersn

sandersn Feb 23, 2017

Member

Can you move this test case to thisTypeInAccessors.ts so that we can see the types?

@sandersn

sandersn Feb 23, 2017

Member

Can you move this test case to thisTypeInAccessors.ts so that we can see the types?

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 25, 2017

Member

With latest commits we now use the contextual type of an object literal as this in methods of the object literal. The PR description has been updated with the exact rules.

Member

ahejlsberg commented Feb 25, 2017

With latest commits we now use the contextual type of an object literal as this in methods of the object literal. The PR description has been updated with the exact rules.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 25, 2017

Member

@sandersn I have addressed the code review comments. With the change to use the contextual type of object literals for this we probably want to re-run the RWC tests and see where we stand now.

Member

ahejlsberg commented Feb 25, 2017

@sandersn I have addressed the code review comments. With the change to use the contextual type of object literals for this we probably want to re-run the RWC tests and see where we stand now.

ahejlsberg added some commits Feb 27, 2017

@sandersn

This comment has been minimized.

Show comment
Hide comment
@sandersn

sandersn Feb 27, 2017

Member

Here are updated results from real-world code. To summarise, the previous contextual typing issues are fixed without losing the good breaks. I also found three more breaks that I must have overlooked in the previous pass.

The last break is interesting because it is of this form:

declare class C {
  element: JQuery;
  handler(): void;
}
class D extends C {
  init() {
    this.element.on("something", "something else", this.handler = function () {
      // handler code ...
    });
  }
}

The problem is that the nested assignment of this.handler = function ... causes the handler to have this: this even though it should be HTMLElement. Notice that C.handler shouldn't have this: this either, but this: HTMLElement. Now the definition of C needs to be:

class C {
  element: JQuery;
  handler(this: this): void;
}

Fixed

  1. Angular's react perf comparison now picks up props from the contextual type.
  2. Mockjax usage now picks up responseText from the contextual type.

Bugs

  1. Bug in VS Code's markdownWorker.ts is still detected.
  2. Bug in RxJs's AjaxObservable.ts is still detected.
  3. Bug with the Draggable example is still detected. (+5 other repros)
  4. Bug in C/CBase mixup example is still detected.

Needs updated d.ts

  1. Knockout's missing call signature is still detected.
  2. Object.defineProperty/Properties is still missing its contextual type.

More annotations required

  1. Angular's decorator_spec break is still detected.
  2. Here is the simplified code, which appears to be drawing a data series:
private CreateSeries(data: Array<any>) {
    var self = this;
    var options = self.options;
    var series = {
        data: data,
        dataLabels: {
            useHTML: true,
            formatter: function () {
                if (this.point.count > 0) {
                         ~~~~~
!!! error TS2339: Property 'point' does not exist on type '{ useHTML: boolean; formatter: () => any; enabled: boolean; }'.
                    return this.point.toHtml();
                                ~~~~~
!!! error TS2339: Property 'point' does not exist on type '{ useHTML: boolean; formatter: () => any; enabled: boolean; }'.
                }
            },
            enabled: options.showDataLabels
        }
    };
    return series;
}

I can't tell if this is a bug or a place that needs a this annotation now. I don't think it is a bug, because it's a lot of code never to have been used. So I think formatter is designed for use as a callback. But I can't navigate through the code in RWC's stored form, so I haven't been able to confirm whether formatter is ever pulled off and called with a this of the right type.

  1. Bug in insane code

I'm not sure why I missed this bug in the previous pass:

// in a d.ts ...

class AsIs {
  cleanup(): void;
  // some other static functions ...
}
// in a function ...
var foo = new AsIs();
foo.cleanup = function () 
{
    var container: HTMLElement = this.arguments.container;
    // more code follows ...
}
Manager.register(foo);

// much later
public doStuff(container: HTMLElement, context: any)
{
    // do some stuff ...
    AsIs.prototype.cleanup.call(context);
}

This code doesn't look like it should work. It's insane in a way that's only possible in Javascript:

  1. In a d.ts, class AsIs declares that it has a cleanup method.
  2. Some function creates var foo = new AsIs()
  3. Then it assigns a new function to foo.cleanup, which puts it on the instance.
  4. Then it registers this instance with some state manager.
  5. Much later, other code runs AsIs.prototype.cleanup.call(context). Which skips the previously-assigned instance function entirely. And context is of type any, so it's anybody's guess whether it's actually of type AsIs.

The error that shows up on the assignment is good because it should cause the authors to re-assess whether cleanup is even used at all. Which it doesn't appear to be.

  1. this parameter annotation needed:
this.element.on("mouseenter.grid", "style .xxx", this._handler = function() {
    var $this = $(this);
    if ($this.attr('title') !== $this.text()) {
        if (this.offsetWidth < this.scrollWidth) {
                 ~~~~~~~~~~~
!!! error TS2339: Property 'offsetWidth' does not exist on type 'Widget'.
                                    ~~~~~~~~~~~
!!! error TS2339: Property 'scrollWidth' does not exist on type 'Widget'.
            $this.attr('title', $this.text());
        }
    }
}

The inline this._handler = function ... assignment causes this to be contextually typed as the containing class, but I'm pretty sure that this is supposed to be HTMLElement. (I don't know JQuery so I'm not completely sure.)

The same breaks happens a few more times in this project.

Member

sandersn commented Feb 27, 2017

Here are updated results from real-world code. To summarise, the previous contextual typing issues are fixed without losing the good breaks. I also found three more breaks that I must have overlooked in the previous pass.

The last break is interesting because it is of this form:

declare class C {
  element: JQuery;
  handler(): void;
}
class D extends C {
  init() {
    this.element.on("something", "something else", this.handler = function () {
      // handler code ...
    });
  }
}

The problem is that the nested assignment of this.handler = function ... causes the handler to have this: this even though it should be HTMLElement. Notice that C.handler shouldn't have this: this either, but this: HTMLElement. Now the definition of C needs to be:

class C {
  element: JQuery;
  handler(this: this): void;
}

Fixed

  1. Angular's react perf comparison now picks up props from the contextual type.
  2. Mockjax usage now picks up responseText from the contextual type.

Bugs

  1. Bug in VS Code's markdownWorker.ts is still detected.
  2. Bug in RxJs's AjaxObservable.ts is still detected.
  3. Bug with the Draggable example is still detected. (+5 other repros)
  4. Bug in C/CBase mixup example is still detected.

Needs updated d.ts

  1. Knockout's missing call signature is still detected.
  2. Object.defineProperty/Properties is still missing its contextual type.

More annotations required

  1. Angular's decorator_spec break is still detected.
  2. Here is the simplified code, which appears to be drawing a data series:
private CreateSeries(data: Array<any>) {
    var self = this;
    var options = self.options;
    var series = {
        data: data,
        dataLabels: {
            useHTML: true,
            formatter: function () {
                if (this.point.count > 0) {
                         ~~~~~
!!! error TS2339: Property 'point' does not exist on type '{ useHTML: boolean; formatter: () => any; enabled: boolean; }'.
                    return this.point.toHtml();
                                ~~~~~
!!! error TS2339: Property 'point' does not exist on type '{ useHTML: boolean; formatter: () => any; enabled: boolean; }'.
                }
            },
            enabled: options.showDataLabels
        }
    };
    return series;
}

I can't tell if this is a bug or a place that needs a this annotation now. I don't think it is a bug, because it's a lot of code never to have been used. So I think formatter is designed for use as a callback. But I can't navigate through the code in RWC's stored form, so I haven't been able to confirm whether formatter is ever pulled off and called with a this of the right type.

  1. Bug in insane code

I'm not sure why I missed this bug in the previous pass:

// in a d.ts ...

class AsIs {
  cleanup(): void;
  // some other static functions ...
}
// in a function ...
var foo = new AsIs();
foo.cleanup = function () 
{
    var container: HTMLElement = this.arguments.container;
    // more code follows ...
}
Manager.register(foo);

// much later
public doStuff(container: HTMLElement, context: any)
{
    // do some stuff ...
    AsIs.prototype.cleanup.call(context);
}

This code doesn't look like it should work. It's insane in a way that's only possible in Javascript:

  1. In a d.ts, class AsIs declares that it has a cleanup method.
  2. Some function creates var foo = new AsIs()
  3. Then it assigns a new function to foo.cleanup, which puts it on the instance.
  4. Then it registers this instance with some state manager.
  5. Much later, other code runs AsIs.prototype.cleanup.call(context). Which skips the previously-assigned instance function entirely. And context is of type any, so it's anybody's guess whether it's actually of type AsIs.

The error that shows up on the assignment is good because it should cause the authors to re-assess whether cleanup is even used at all. Which it doesn't appear to be.

  1. this parameter annotation needed:
this.element.on("mouseenter.grid", "style .xxx", this._handler = function() {
    var $this = $(this);
    if ($this.attr('title') !== $this.text()) {
        if (this.offsetWidth < this.scrollWidth) {
                 ~~~~~~~~~~~
!!! error TS2339: Property 'offsetWidth' does not exist on type 'Widget'.
                                    ~~~~~~~~~~~
!!! error TS2339: Property 'scrollWidth' does not exist on type 'Widget'.
            $this.attr('title', $this.text());
        }
    }
}

The inline this._handler = function ... assignment causes this to be contextually typed as the containing class, but I'm pretty sure that this is supposed to be HTMLElement. (I don't know JQuery so I'm not completely sure.)

The same breaks happens a few more times in this project.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Feb 28, 2017

Member

Latest commit introduces a new CheckMode enum and substitutes a checkMode parameter for the contextualMapper parameter in a number of checkXXX methods. Also, a new getContextualMapper function is introduced that can be called to obtain the contextual type mapper (or an identity mapper if there is no contextual type mapper). Finally, the getContextualThisParameterType function is modified to instantiate the T obtained from a ThisType<T> using the contextual type mapper.

Also, tests were added to validate we can accurately type Object.defineProperty and Object.defineProperties.

Member

ahejlsberg commented Feb 28, 2017

Latest commit introduces a new CheckMode enum and substitutes a checkMode parameter for the contextualMapper parameter in a number of checkXXX methods. Also, a new getContextualMapper function is introduced that can be called to obtain the contextual type mapper (or an identity mapper if there is no contextual type mapper). Finally, the getContextualThisParameterType function is modified to instantiate the T obtained from a ThisType<T> using the contextual type mapper.

Also, tests were added to validate we can accurately type Object.defineProperty and Object.defineProperties.

@@ -433,6 +433,12 @@ namespace ts {
ResolvedReturnType
}
+ const enum CheckMode {

This comment has been minimized.

@sandersn

sandersn Mar 1, 2017

Member

Shouldn't this go in types.ts?

@sandersn

sandersn Mar 1, 2017

Member

Shouldn't this go in types.ts?

This comment has been minimized.

@ahejlsberg

ahejlsberg Mar 1, 2017

Member

No, it's basically an internal implementation detail of the type checker.

@ahejlsberg

ahejlsberg Mar 1, 2017

Member

No, it's basically an internal implementation detail of the type checker.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Mar 2, 2017

Member

Latest commit puts all of the new functionality under the --noImplicitThis switch. This ensures there are no breaking changes as a result of this PR since the situations in which we strongly type this in object literal methods were previously errors under --noImplicitThis.

Member

ahejlsberg commented Mar 2, 2017

Latest commit puts all of the new functionality under the --noImplicitThis switch. This ensures there are no breaking changes as a result of this PR since the situations in which we strongly type this in object literal methods were previously errors under --noImplicitThis.

@ahejlsberg ahejlsberg added this to the TypeScript 2.3 milestone Mar 2, 2017

@aozgaa

This comment has been minimized.

Show comment
Hide comment
@aozgaa

aozgaa Mar 2, 2017

Contributor

In the SymbolDisplayBuilder, should we change the following?

                    else if (type.flags & TypeFlags.TypeParameter && (type as TypeParameter).isThisType) {
                        if (inObjectTypeLiteral) {
                            writer.reportInaccessibleThisError();
                        }
                        writer.writeKeyword("this");
                    }
Contributor

aozgaa commented Mar 2, 2017

In the SymbolDisplayBuilder, should we change the following?

                    else if (type.flags & TypeFlags.TypeParameter && (type as TypeParameter).isThisType) {
                        if (inObjectTypeLiteral) {
                            writer.reportInaccessibleThisError();
                        }
                        writer.writeKeyword("this");
                    }
@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Mar 2, 2017

Member

@aozgaa No, that is an unrelated check (it reports an error if this is accessed in a type position within an object type literal).

Member

ahejlsberg commented Mar 2, 2017

@aozgaa No, that is an unrelated check (it reports an error if this is accessed in a type position within an object type literal).

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Mar 2, 2017

Member

@mhegazy I think this one is good to do. Want to take a look?

Member

ahejlsberg commented Mar 2, 2017

@mhegazy I think this one is good to do. Want to take a look?

@mhegazy

mhegazy approved these changes Mar 6, 2017

@ahejlsberg ahejlsberg merged commit 41226d0 into master Mar 6, 2017

4 checks passed

TypeScript Test Run typescript_node.4 Build finished.
Details
TypeScript Test Run typescript_node.6 Build finished.
Details
TypeScript Test Run typescript_node.stable Build finished.
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details

@ahejlsberg ahejlsberg deleted the contextualThisType branch Mar 6, 2017

@kitsonk kitsonk referenced this pull request in dojo/meta Mar 9, 2017

Closed

Migration to TypeScript 2.3 #153

@octref octref referenced this pull request in vuejs/vetur Mar 17, 2017

Merged

Add vue.d.ts and typescript support #94

@mattiamanzati mattiamanzati referenced this pull request in mobxjs/mobx-state-tree Apr 26, 2017

Closed

introduce utility methods? #84

@bryanbecker bryanbecker referenced this pull request in screepers/screeps-typescript-starter May 17, 2017

Closed

Tasks cleanup #44

@dwickern

This comment has been minimized.

Show comment
Hide comment
@dwickern

dwickern Jun 13, 2017

In this scenario, is it possible to infer this from the containing object literal? Or declare somehow that wrap does not change the context?

let foo = {
    hello: "hello",
    f() {
        return this.hello; // works
    },
    get p() {
        return this.hello; // works
    },
    g: wrap(function () {
        return this.hello; // TS2683:'this' implicitly has type 'any' because it does not have a type annotation.
    })
}

In this scenario, is it possible to infer this from the containing object literal? Or declare somehow that wrap does not change the context?

let foo = {
    hello: "hello",
    f() {
        return this.hello; // works
    },
    get p() {
        return this.hello; // works
    },
    g: wrap(function () {
        return this.hello; // TS2683:'this' implicitly has type 'any' because it does not have a type annotation.
    })
}
@Kovensky

This comment has been minimized.

Show comment
Hide comment
@Kovensky

Kovensky Jun 13, 2017

Contributor

I haven’t tested it but it should be possible with generics.

declare function wrap<T, F extends (this: T) => any>(f: F): F
Contributor

Kovensky commented Jun 13, 2017

I haven’t tested it but it should be possible with generics.

declare function wrap<T, F extends (this: T) => any>(f: F): F
@dwickern

This comment has been minimized.

Show comment
Hide comment
@dwickern

dwickern Jun 13, 2017

Unfortunately that doesn't seem to work without specifying wrap<{ hello: string }>(function() { ... }).

Unfortunately that doesn't seem to work without specifying wrap<{ hello: string }>(function() { ... }).

@mattmccutchen mattmccutchen referenced this pull request in Microsoft/TypeScript-Handbook Sep 21, 2017

Open

Document `ThisType<T>` #649

@tomcon tomcon referenced this pull request in sveltejs/svelte Nov 28, 2017

Open

Types #418

@Microsoft Microsoft locked and limited conversation to collaborators Jun 19, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.