microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.12k stars 12.29k forks source link

Make Javascript intellisense detect inheritance #13206

Open mjbvz opened 7 years ago

mjbvz commented 7 years ago

From @benjaminmillhouse on December 21, 2016 17:33

Extension Author Version
JSDocTagComplete HookyQR 0.0.2
html-css-class-completion Zignd 1.0.3
theme-atom-one-dark andischerer 0.0.1
vscode-eslint dbaeumer 1.1.0
docthis joelday 0.3.5
Angular1 johnpapa 0.1.16
Angular2 johnpapa 1.0.2
csharp ms-vscode 1.5.3
js-atom-grammar ms-vscode 0.1.10
vscode-icons robertohuertasm 4.0.2
bootstrap-3-snippets wcwhitehead 0.0.8

Hello, I would love it if VS Code's Javascript intellisense was able to detect inheritance. Please let me know if there is a way to accomplish this today through jsdoc comments or anything like that. I have tried all kinds of different ways to get VS Code to detect it (definitions like below, @augments, @extends jsdoc comments, etc) and have not gotten anything to work.

As an example, when inheriting like this:
function BaseClass() {
  this.foo = 'bar';
}
BaseClass.prototype.baz = function () { }

function InheritedClass() {
  BaseClass.call(this);
}
InheritedClass.prototype = Object.create(BaseClass.prototype);
InheritedClass.prototype.constructor = InheritedClass;

Would like the intellisense for new InheritedClass(). to show 'foo' and 'baz' as properties/methods for InheritedClass in Intellisense.

Copied from original issue: Microsoft/vscode#17690

mhegazy commented 7 years ago

https://github.com/Microsoft/TypeScript/pull/12441 adds support for @augments for ES6 classes, but not for functions. we should add this as well.

MartB commented 7 years ago

How is the state on this (apparently its not important as its getting moved from milestone to milestone) ? Is there any workaround available ?

Kind regards Martin

PhaserEditor2D commented 7 years ago

Yes, actually this is the only one issue that stop us to use Salsa as default editor in Phaser Editor :(

vincentvictoria commented 5 years ago

@mjbvz mentioned

There does not seem to be a way to document this inheritance using JSDoc currently.

JSDoc now has a way using augments or extends:

/**
 * @constructor
 */
function Animal() {
    /** Is this animal alive? */
    this.alive = true;
}

/**
 * @constructor
 * @augments Animal
 */
function Duck() {}
Duck.prototype = new Animal();

Typescript's jsdoc support page mentions it, but it's trivial

/**
 * @template T
 * @extends {Set<T>}
 */
class SortableSet extends Set {
  // ...
}

So vscode doesn't support the classic way of using the tags and displays this:

untitled 2

Even if it's hard for the intellisense to detect the classic prototype chain from codes, isn't it easier to detect from JSDoc's augments tag?

sandersn commented 5 years ago

Yes, it is very much easier, although detection of inheritance patterns isn't the main obstacle to this feature. The compiler doesn't treat constructor functions the same as classes internally. It has a special-case code path in checkNewExpression that lets constructor functions be newed. There is lot of work needed to convert constructor functions to be real classes.

vincentvictoria commented 5 years ago

The compiler doesn't treat constructor functions the same as classes internally. It has a special-case code path in checkNewExpression that lets constructor functions be newed.

Okay, I see why it's being problematic. I hope there's an easier way to convert constructor functions to classes but it seems to be very dependent on implementation details.

sandersn commented 5 years ago

Blocked by #26883; changing to constructor functions to be real classes will primarily involve fixing (1) how they treat inheritance (2) making them generic, including this types.

Pandawan commented 4 years ago

Any updates on this? I'm using a library that has ES5 classes with constructor functions and would love for the intellisense to work when it extends other objects.

I know this issue is blocked until #26883 is fixed, but neither of them have been updated for a while now.

I also found that #18609 and #30943 reference the same issue.

sandersn commented 4 years ago

@PandawanFr 3.7 makes constructor functions into real classes (although type parameters still aren't correctly instantiated), so the main obstacle to this feature is now identifying and ranking Javascript inheritance patterns.

This is a matter of corpus linguistics on code, which means that the first tool to verify your hypotheses is grep. If you're interested, you could pretty easily figure out which patterns are most common. (I don't have time to do it unless we decide to put this feature in our 3.8 or 3.9 plans.)

4r7d3c0 commented 4 years ago

This hack over prototype assignment works for me. Still isn't perfect for the constructor, but at least all other methods inherit JSDoc.

import aqt from '@rqt/aqt'
import { WushInterface } from '../types/lux'

export default class Wush extends WushInterface {
  constructor() {
    super()
  }
}

Object.defineProperties(Wush.prototype, Object.getOwnPropertyDescriptors(Wush.prototype = /** @type {!Wush} */ ({
  async sendNotification(subscription, payload, options) {
    const requestDetails = this.generateRequestDetails(subscription, payload, options)
    const res = await aqt(requestDetails.endpoint)

    const { statusCode, body, headers } = res
    if (statusCode < 200 || statusCode > 299) {
      throw new Error(
        'Received unexpected response code',
      )
    }
    return res
  },
  generateRequestDetails(subscription, payload, options) {
        // implement me
  },
})))

I'm doing this because I generate interfaces which are then provided an implementation with:

Show interface ```js import { Readable } from 'stream' /** * An interface for sending web push notifications. * @implements {_wush.Wush} */ export class WushInterface extends Readable { /** * Constructor method. */ constructor(...args) { super(...args) } // hello() { // return 'world' // } /** * Sends a push notification to a subscription, optionally with a payload and options. * @param {!_wush.PushSubscription} subscription The _PushSubscription_ record that will receive the notification. * @param {(string|!Buffer)} payload The data to transmit to the user via the endpoint. * @param {!Object} options Options for the GCM API key and VAPID keys can be passed here to override the default initialised options. * @return {!Promise<{ body: string, status: number, headers: !Object }>} */ async sendNotification(subscription, payload, options) { } /** * Returns the details for a push message request, without sending the actual notification. * @param {!_wush.PushSubscription} subscription The _PushSubscription_ record that will receive the notification. * @param {(string|!Buffer)} payload The data to transmit to the user via the endpoint. * @param {!Object} options Options for the GCM API key and VAPID keys can be passed here to override the default initialised options. * @return {{ endpoint: string, method: string, headers: !Object, payload: (string|!Buffer) }} */ generateRequestDetails(subscription, payload, options) { } } /** * @suppress {nonStandardJsDocs} * @typedef {import('../').PushSubscription} _wush.PushSubscription */ ```

The Wush.prototype = /** @type {!Wush} */ is actually used for Closure Compiler type checking:

src/wush.js:23: WARNING - [JSC_TYPE_MISMATCH] inconsistent return type
found   : null
required: (IThenable<{
  body: string,
  headers: Object,
  status: number
}>|{
  body: string,
  headers: Object,
  status: number
})
    return null
           ^^^^

It's a bit of a hustle to define classes like that but if it's the only thing that works then there isn't any other solution right now. This workaround makes coding for interfaces much easy.

image

trusktr commented 3 years ago

JSDoc is not supported inside .ts files, so @constructor and @augments.... don't do anything in.... .ts files. LOL

What can community members do to encourage ES5 support? Can we donate money towards particular issues? There has to be some way to get these things prioritized.

Obscerno commented 3 years ago

Like everyone here, this issue has caused me a lot of grief. It would be nice to see it fixed one day.

I'll share my workaround below for psuedo-classical inheritance. You could probably do something similar with prototypal inheritance. It's just a bit of code in the child object wrapped in a region, which is then removed from the code completely on build.

    /** The parent object.
     *  @constructor 
     *  @param {*} arg - A parameter that has to be passed in on construction. */
    function Parent(arg) {
        /** Public function. In the documentation, this description will carry
         *  over to the child object. In the IDE, only typechecking will.
         *  @param {string} name - User's name.
         *  @returns {string} string **/
        this.hello = function(name) {
            return "Hello " + name;
        }
    }

    /** The child object. 'augments' is what jsdocs uses to detect inheritance.
     *  'augments' creates an IDE error, which the ts-ignore flag suppresses.
     *  @constructor
     *  @param {*} arg - A parameter that has to be passed in on construction.
     *  @augments Parent
     *  @ts-ignore */
    function Child(arg) {
        Parent.call(this, arg);

        // Remove the IDE Hack region on build, otherwise it'll mess up your code.

        //#region IDE Hack
        var dummy = new Parent(null);
        this.hello = dummy.hello;
        //#endregion

        // New code goes below . . . 
    }

In the IDE, type checking and parameter descriptions carry over. The descriptions for the properties themselves do not carry over in the IDE, but they do in the documentation.

adschm commented 2 years ago

Thanks @Obscerno for this pointer. This is a derived option which does not need require to edit the file during build, but will produce some useless JS code eventually (though the code is never executed):

    function Child(arg) {
        Parent.call(this, arg);

        // Dummy section to initialize properties from superclass

        // eslint-disable-next-line
        if (false) {
                // @ts-ignore
                var dummy = new Parent(null);
                this.hello = dummy.hello;
        }

        // New code goes below . . . 
    }

Fortunately, TypeScript is broken enough so 1. the definitions are picked up inside the dead block and 2. one ts-ignore is enough even if you have multiple lines inside the block. (The latter is quite helpful since the silly ts-ignore has no options or block-treatment. Unbelievable ...)

Edit/Update:

  1. Maybe one can even remove this dead condition with a good minimizer (I'm using WebOptimizer for ASP Core which does not).
  2. The if(false) hack does not work for function calls which are directly in the constructor:
    function Child(arg) {
        Parent.call(this, arg);

        // Dummy section to initialize properties from superclass

        // eslint-disable-next-line
        if (false) {
                // @ts-ignore
                var dummy = new Parent(null);
                this.hello = dummy.hello;
        }

        // New code goes below . . . 

        this.hello("abc"); // This will trigger: TS2722: Cannot invoke an object which is possibly 'undefined'

        this.anotherFunction = function() {
                this.hello("def"); // This triggers no warning and has correct type/Intellisense comment
        }
    }