简体   繁体   中英

JSDoc return instance of generic type

In short, I'm looking to pass a generic type into a factory's constructor and have the factory return instances of the generic type.

As a small bonus, the generic I'm passing in is a class extension itself. The goal is to have the AccountService.getOne() method return an instance of Account - which would then have access to it's particular methods.

The closest I've managed to get, is what you'll find below, where it's returning an instance of AccountService instead of Account

Please find an SSCCE below, I'm happy to answer any and all questions

Thanks in advance

PS: I have consulted a fair few resources on JSDoc so far, but the abstract examples aren't much help to me (yet)

https://github.com/google/closure-compiler/wiki/Generic-Types

https://jsdoc.app

https://medium.com/@antonkrinitsyn/jsdoc-generic-types-typescript-db213cf48640 (i know)

/**
 * An abstract class representing a DB record
 * @class
 */
class AbstractDataObject {
    constructor() {}

    save() {}

    update() {}

    delete() {}
}

/**
 * An abstract service to retrieve DB records and return them as AbstractDataobjects
 * @class
 * @template T
 */
class AbstractDataService {

    /**
     * @param {T} classType The data class
     */
    constructor (classType) {
        this.classType = classType
    }

    /**
     * @returns {T} Returns a new instance of the provided classType
     */
    getOne () {
        return new this.classType() // I assumed this would return it as an instance of the generic, alas
    }
}

/**
 * @class
 * @extends AbstractDataObject<Account>
 */
class Account extends AbstractDataObject {
    constructor () {
        super ()
    }
}

/**
 * @class
 * @extends AbstractDataService<AccountService>
 */
class AccountService extends AbstractDataService {
    constructor () {
        super (Account)
    }
}


const accountService = new AccountService()
const account = accountService.getOne()
account. // Expect to see .save(), .update(), .delete() here, yet it is of type AccountService

As far as I know JSDoc and Google Closure Compiler try to support each other annotations and expressions but they did diverge at some point. For most common use cases you don't have to worry about interoperability but, as far as I am aware, the @template tag is a GCC tag only.

However I think you could achieve the same thing with @interface and @implements :

/** @interface */
class AbstractDataObject {
    save() {}
    update() {}
    delete() {}
}

/** @interface */
class AbstractDataService {
    getOne () {}
}

/** @implements {AbstractDataObject} */
class Account extends AbstractDataObject {}

/** @implements {AbstractDataService} */
class AccountService extends AbstractDataService {
    /** @return {Account} */
    getOne() {
      return new Account();
    }
}

VS Code IntelliSense had no issues with that:

在此处输入图像描述

I'm not a JSDoc expert, so I can't help with the JSDoc part, but I think it's fair to assume that we should make sure this work in clean TypeScript first. Then hopefully you'll find describing it in JSDoc easier.

After some fiddling, I found that the crucial part is to differentiate between "Account as a type" (ie the definition of how each single instance behaves) and "Account as a value" (ie an actual material JS object, the class prototype, which knows how to construct new Account instances).

Here's the full working example in TS:

// An *instance* of a DataObject should have following methods implemented.
interface DataObject {
    save(): void;
    update(): void;
    delete(): void;
}

// A *prototype* of a DataObject should support the "new" keyword in order to create new *instances* of that DataObject.
interface DataObjectConstructable<T extends DataObject> {
    new(): T;
}

// This factory of a DataObject knows the type of *DataObject instances* it generates,
// and also that there is a *DataObject prototype* it will need to use in order to do that.
class DataObjectFactory<T extends DataObject, K extends DataObjectConstructable<T>> {
    constructable: K;

    constructor (constructable: K) {
        this.constructable = constructable;
    }

    getOne(): T {
        return new this.constructable()
    }
}

// An *instance* of Account is a DataObject, which means it has to implement specific methods.
// AFAIR, the constructor is technically owned by the *class prototype* and not the instances, but we're still able to define it here as a shortcut.
class Account implements DataObject {
    constructor() {}
    save() {}
    update() {}
    delete() {}
}

// This specific Factory knows that it will generate Account instances (Account class definition used as a type).
// In order to do that, it uses the Account prototype (Account class prototype used as a value, an actual object that can be stored in the DataObjectFactory instance by reference).
class AccountService extends DataObjectFactory<Account, DataObjectConstructable<Account>> {
    constructor () {
        super (Account);
    }
}

const accountService = new AccountService()
const account = accountService.getOne()
account. // .save(), .update(), .delete() are autocompleted as expected.

You can check it out in action here: TS Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM