GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Allow to explicitly pass type parameters via JSDoc · Issue #27387 · microsoft/TypeScript

Allow to explicitly pass type parameters via JSDoc #27387

New issue
Copy link
New issue
Copy link
Open
Open
Allow to explicitly pass type parameters via JSDoc#27387
Copy link
Labels
Domain: JSDocRelates to JSDoc parsing and type generationRelates to JSDoc parsing and type generationDomain: JavaScriptThe issue relates to JavaScript specificallyThe issue relates to JavaScript specificallyIn DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScriptcheckJsRelates to checking JavaScript using TypeScriptRelates to checking JavaScript using TypeScript

Description

@ifeltsweet
Denis Pshenov (ifeltsweet)
opened on Sep 27, 2018
Issue body actions

Search Terms

jsdoc generics type parameters constraints

Suggestion

It seems like it is not possible via JSDoc to explicitly tell compiler which type parameters to pass to a generic function.

Use Cases

In TypeScript it is possible to explicitly pass type parameters such as mongoose.model<Model, Schema>('modelName', Schema) while I could not find a way to do same with JSDoc.

Examples

mongoose.model has two signatures, first one with one type parameter and the other with two. To make use of the second signature we must pass types explicitly.

export function model<T extends Document>(
  name: string,
  schema?: Schema,
  collection?: string,
  skipInit?: boolean
): Model<T>;

export function model<T extends Document, U extends Model<T>>(
  name: string,
  schema?: Schema,
  collection?: string,
  skipInit?: boolean
): U;
//  Choose second signature in typescript.
const model = mongoose.model<Model, Schema>('modelName', Schema);

// With JSDoc, compiler always chooses first signature and we receive a type mismatch error.
/** @type {Schema & mongoose.Model<Model>} */
const model = mongoose.model('modelName', Schema);

// But something like this would be great.
const model = mongoose.model/**<Model, Schema>*/('modelName', Schema);

My apologies if this is already possible, but I've spend almost a week battling this.

Related: https://github.com/Microsoft/TypeScript-Node-Starter/issues/101

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
Reacted by Stuart Knightley, Nicu Chiciuc, Chiawen Chen, Eugene Kulabuhov, Yeison Daza, Vincent Côté-Roy, Paul Li, Bo Lingen, Lior Cohen, Ayan Yenbekbay and 149 more
Reacted by Ricky Boyce
Reacted by luketanner-sq, Kyℓe Hensel, Joe Pea, Luke Tanner, Lucca Mordente, Tim, Petr Krejcik, Junior Usca, Tomáš Hübelbauer, Samuel Bronson and 14 more
Reacted by Kyℓe Hensel, Lance Vance, Junior Usca, Ricardo Medina, Bernard, lommi, Ricky Boyce and Wouter Heirstrate

Activity

Next
  1. ghost added
    SuggestionAn idea for TypeScriptAn idea for TypeScript
    on Sep 27, 2018
  2. ghost added
    Domain: JavaScriptThe issue relates to JavaScript specificallyThe issue relates to JavaScript specifically
    on Sep 27, 2018
  3. ghost added
    Salsa
    on Sep 27, 2018
  4. ghost added
    Domain: JSDocRelates to JSDoc parsing and type generationRelates to JSDoc parsing and type generation
    on Sep 27, 2018
  5. ghost added
    checkJsRelates to checking JavaScript using TypeScriptRelates to checking JavaScript using TypeScript
    on Sep 27, 2018
  6. Wesley Wigham (weswigham)
    added
    In DiscussionNot yet reached consensusNot yet reached consensus
    on Nov 6, 2018
  7. Wesley Wigham (weswigham)
    added
    Domain: JavaScriptThe issue relates to JavaScript specificallyThe issue relates to JavaScript specifically
    and removed
    Domain: JavaScriptThe issue relates to JavaScript specificallyThe issue relates to JavaScript specifically
    Salsa
    on Nov 29, 2018
  8. Stuk commented on Mar 12, 2019

    @Stuk
    Stuart Knightley (Stuk)
    on Mar 12, 2019
    More actions

    I would also like to see something like this. Currently it's really hard to use functions with generic types from JS.

    Reacted by Szymon Dzialowski, Matthew Taylor and ExE Boss
  9. ekulabuhov commented on Nov 7, 2019

    @ekulabuhov
    Eugene Kulabuhov (ekulabuhov)
    on Nov 7, 2019
    More actions

    Another use case with React Hooks:

    // useState<string> is derived correctly
    const [aString, setAString] = useState("default value");

    Removing default value there is no way to pass type info:

    // useState<any>
    const [aString, setAString] = useState();
    Reacted by Bnaya Peretz, Bo Lingen, Mahatthana (Kelvin) Nomsawadi, Michael H, Dan Prince, Kyℓe Hensel, Ben, Albert Mañosa, William Bert, Seb Insua and 22 more
  10. pm0u commented on May 1, 2020

    @pm0u
    Paul Mourer (pm0u)
    on May 1, 2020
    More actions

    Anything on this? Also stuck on the useState example as shared above:

    // Doesn't set type properly
    const [error, setError] = /** @type {React.useState<?string>} */ useState(null);
    // Nor does this
    /** @type {[?string, React.Dispatch<React.SetStateAction<?string>>]} */
    const [error, setError] = /** @type {React.useState<?string>} */ useState(null);
    
    
    // we have an error we want to set now.
    setError('Error!') // Shows a type error

    Edit: Ok this might be a hack, but in the case of the useState example it works:

    const [error, setError] = useState(/** @type {?string} */ (null));

    implied types for error are string | null and setError is React.Dispatch<React.SetStateAction<?string>>

    i think what's happening "under the hood" is we are forcing the "type" that useState is being passed.

    Reacted by Michael Irwin and George Douglas Snyder
  11. robertknight commented on Jul 16, 2020

    @robertknight
    Robert Knight (robertknight)
    on Jul 16, 2020
    More actions

    For functions that take an argument of the generic type, casting the argument itself is reasonably practical. We've settled on these patterns with useState and useRef in React for example:

    const [thing, setThing] = useState(/** @type {SomeType|null} */ (null));
    const widgetRef = useRef(/** @type {HTMLElement|null} */(null));

    In cases where there is no argument for everything else to be inferred from, it gets much more verbose unfortunately. What would be helpful is being able to do something like:

    const aSet = /** @type {?<string>} */(new Set());

    Where the ? is inferred as Set. In this example it only saves a few characters, but it could be a lot more if the generic type name is lengthy and/or has to be imported from somewhere.

    Reacted by Kyℓe Hensel, William Bert, bmartinelli, José Fernando Höwer Barbosa, Robbie Cook, Petr Krejcik, Sepehr Soltanieh, Richard Roncancio, Francesc Rosas, Yuji Sugiura and 16 more
    Reacted by Robbie Cook, Sepehr Soltanieh, Junior Usca and Tomasz Lenarcik
    Reacted by Robbie Cook, Sepehr Soltanieh and Junior Usca
  12. 17 remaining items

  13. noinkling commented on Oct 18, 2023

    @noinkling
    Malcolm (noinkling)
    on Oct 18, 2023
    More actions

    Pascal Schilp (@thepassle) Technically this is already solved as per #27387 (comment) - not really sure why this issue hasn't been closed yet.

    For example you can now do:

    const model = /** @type {typeof mongoose.model<Model, Schema>} */(mongoose.model)('modelName', Schema);

    If you're aware of this but your argument is that you would prefer for there to be a more ergonomic syntax, probably best to say that explicitly.

    Reacted by Gustavo, Int, Jarrod Davis and lue
  14. thepassle commented on Oct 18, 2023

    @thepassle
    Pascal Schilp (thepassle)
    on Oct 18, 2023
    More actions

    Those indeed seem more like workarounds, so I understand the issue is still open.

    So yes, I would prefer for there to be a more ergonomic syntax.

    Reacted by Bjornar Egede-Nissen, Ilia Andrienko, Jonathan Loss and Julian Cataldo
  15. mhofman commented on Oct 18, 2023

    @mhofman
    Mathieu Hofman (mhofman)
    on Oct 18, 2023
    More actions

    Malcolm (@noinkling), those are not equivalent. A generic method would become unbound from its target if aliased or type annotated that way.

  16. noinkling commented on Oct 19, 2023

    @noinkling
    Malcolm (noinkling)
    on Oct 19, 2023
    More actions

    Malcolm (@noinkling), those are not equivalent. A generic method would become unbound from its target if aliased or type annotated that way.

    You mean methods won't have the right this value/type, essentially? I can see the issue with the alias approach in those cases, but nothing new there. Can you give an example where the casting syntax could be problematic/have a different result compared to TS syntax?

    In my example both references to the method (runtime and in the annotation) are accessed directly from the object using dot notation (mongoose.model) so I don't see an issue, but maybe I'm missing something.

    If you're concerned about the parentheses they don't change anything:

    const foo = {
      bar: function() { return this === foo }
    };
    
    (foo.bar)();  // true
  17. gustavopch commented on Oct 19, 2023

    @gustavopch
    Gustavo (gustavopch)
    on Oct 19, 2023
    More actions
    const model = /** @type {typeof mongoose.model<Model, Schema>} */(mongoose.model)('modelName', Schema)

    is equivalent to:

    const model = (mongoose.model as typeof mongoose.model<Model, Schema>)('modelName', Schema)

    not to:

    const model = mongoose.model<Model, Schema>('modelName', Schema)

    The fact that this ugly but useful workaround exists doesn't mean a proper syntax for actually passing type parameters (instead of casting the whole function) shouldn't be pursued.

    Reacted by Miroslav Bajtoš, Pascal Schilp, Malcolm, Albert Mañosa, Bernard, Int, Bjornar Egede-Nissen, VDawg, Hyunbin Seo, Karim Jordan and 9 more
  18. noinkling commented on Oct 19, 2023

    @noinkling
    Malcolm (noinkling)
    on Oct 19, 2023
    More actions

    Gustavo (@gustavopch) It didn't exist when this issue was created was the point, it was impossible to provide type params unless they corresponded to runtime params. In that sense the person who created the issue might consider it "solved" now. Personally I've learned to take what I can get when it comes to JSDoc, given that it's something of a second-class citizen for TS. But you're right, it would be nice to have a nicer syntax, just wanted to make it clear that there's an option that works now (even if it isn't pretty).

    Reacted by Albert Mañosa, Bernard and Pete Boere
    Reacted by Int, Jarrod Davis and scarf
  19. phaux commented on Feb 4, 2024

    @phaux
    Niki (phaux)
    on Feb 4, 2024
    More actions

    I return to this thread to point out that the "solution" with instantiation expressions doesn't seem to work when the function has multiple overloads:

    const fileInput =
      /** @type {typeof document.querySelector<HTMLInputElement>} */
      (document.querySelector)("input[type=file]")

    It seems to check all the overloads at once instead of choosing only one:

    Type 'HTMLInputElement' does not satisfy the constraint 'keyof HTMLElementDeprecatedTagNameMap'. deno-ts(2344)
    Type 'HTMLInputElement' does not satisfy the constraint 'keyof HTMLElementTagNameMap'. deno-ts(2344)
    Type 'HTMLInputElement' does not satisfy the constraint 'keyof MathMLElementTagNameMap'. deno-ts(2344)
    Type 'HTMLInputElement' does not satisfy the constraint 'keyof SVGElementTagNameMap'. deno-ts(2344)
    

    In TypeScript it works:

    const fileInput =
      document.querySelector<HTMLInputElement>("input[type=file]")
    Reacted by Malcolm, Julian Cataldo, Vitor L Cavalcanti, Albert Mañosa, Culi T, Changsheng Wu and Nate Kean
  20. noinkling commented on Feb 4, 2024

    @noinkling
    Malcolm (noinkling)
    on Feb 4, 2024
    More actions

    Niki (@phaux) You're right, worth noting it could potentially be considered a bug: #51694

    Reacted by Tomasz Lenarcik
  21. apendua commented on Aug 4, 2024

    @apendua
    Tomasz Lenarcik (apendua)
    on Aug 4, 2024
    Contributor
    More actions

    Nathan Shively-Sanders (@sandersn) I am looking into implementing something like:

    const model =
      /** @specialize {Model, Schema} */
      mongoose.model('modelName', Schema)
    

    Do you think there's a chance this approach will be accepted?

    Reacted by Niki and lue
    Reacted by lue
  22. sandersn commented on Aug 7, 2024

    @sandersn
    Nathan Shively-Sanders (sandersn)
    on Aug 7, 2024
    Member
    More actions

    Our approach to JSDoc has evolved since this issue was created. At that time I didn't want to add anything that didn't have precedent in the jsdoc documentation generator, or at least in Closure compiler. However, Typescript is the largest processor of JSDoc at this point, and TS users who want typing are probably the most prolific authors of it. So we've decided on a few new principles:

    1. If you're exposing existing TS syntax and semantics to JS, the semantics should be identical and the syntax should be as close as possible. @import is a good example, as is your own @overload work.
    2. However, new tags should be normal tags as much as possible. That means:
      a. Starts with @alphanumeric.
      b. Isn't nested -- starts at @ and continues till the next @.
      c. Is used in a /** comment directly above a declaration. Both @import and @overload violate this, for good reasons.

    (1) means that TS users will be able to use the new JS syntax with a minimum of surprise. (2) means that jsdoc processors (including TS itself!) will be able to understand the new tag with a minimum of new parsing.

    Re (2c), the rough descending order of goodness, or familiarity, in my opinion is:

    1. In a /** comment directly preceding a declaration -- like @type.
    2. A /** comment not connected to any JS syntax (which is to say, preceding some non-declaration) -- like @import.
    3. A /** comment preceding an expression -- like @type as a cast.
    4. A /** comment preceding another /** comment that eventually precedes a declaration -- like @overload.
    5. A /** comment preceding some other, non-top-level node in the parse tree.
    6. A /*:: comment or some other new syntax.
    7. A // @ts-alphanumeric comment preceding a declaration.

    I think we can come up with something for (3).

    This is a very long way to say that, yes, we'd consider a tag for this that looks a lot like your @specialise. You're probably going to need parentheses around the call expression to make it attach correctly, though.
    Also I'm not sure whether <Model, Schema> would be better than {Model, Schema}. I think it's down to how hard it is to convert all the type-parameter syntax to that form. The grammar isn't huge there so probably pretty easy.

    Daniel Rosenwasser (@DanielRosenwasser) you've thought about jsdoc a lot too and you may have interesting insights from working on the types-in-JS proposal, which ran into exactly this problem.

    Reacted by ExE Boss and lue
    Reacted by William Bert, Tomasz Lenarcik, Kyℓe Hensel, Miroslav Bajtoš, Mathias Schäfer, Bart Louwers, Pete Boere, Philip Waritschlager, Jesper van den Ende, Anderson Slivinski and 3 more
    Reacted by Kyℓe Hensel, Miroslav Bajtoš, Bart Louwers, Philip Waritschlager, Anderson Slivinski and lue
    Reacted by Max Duval, Bart Louwers, Jesper van den Ende, Anderson Slivinski and lue
    Reacted by Max Duval, Jonathan Tran, Bart Louwers and Philip Waritschlager
  23. apendua commented on Aug 17, 2024

    @apendua
    Tomasz Lenarcik (apendua)
    on Aug 17, 2024
    Contributor
    More actions

    I've got a PR ready for the following syntax:

    let a = /** @specialize <number> */(f());
    
    /** @specialize <string, number> */
    let b = f();
    
    /** @specialize {string} */
    let c = f`template with ${variable}`;
    
    let d = /** @specialize <string> */(new D());

    So basically one can put the @specialize tag at any parent node that accepts JSDoc, e.g. assignment statement or parethesized expression. We can also choose between wrapping type arguments with <...> or {...}.

    This will also work for JSX, though the element with type arguments will need to put inside curly braces so that we can also add JSDoc there:

    function MyForm() {
      return (
        <form>
         {/** @specialize {number} */(
           <Input value={0} />
         )}
        </form>
      )
    }
    Reacted by Jonathan Tran, Seán O'Grady, Christian Meredith, Carlos Martos, Nate Kean and Eli
  24. DanielRosenwasser commented on Aug 19, 2024

    @DanielRosenwasser
    Daniel Rosenwasser (DanielRosenwasser)
    on Aug 19, 2024
    Member
    More actions

    It's something we'll have to consider. It's not perfect, but the solution right now is to cast to a value that's already specialized:

    /**
     * @template T
     * @param x {T}
     */
    function f(x) {
        return x;
    }
    
    let obj = {
        /**
         * @template T
         * @param x {T}
         */
        f(x) {
            return x;
        }
    };
    
    let g = /** @type {typeof f<string>} */ (f);
    //  ^?
    // typeof f<string>
    
    let objG = (/** @type {typeof obj.f<string>} */ (obj.f)).bind(obj);
    //  ^?
    // typeof obj.f<string>

    https://www.typescriptlang.org/play/?filetype=js#code/PQKhFgCgAIWgBALgUwLYAcA2BDF0AqUsC62ATtqtAB7QDe+AvkSMFAGYCuAdgMaIBLAPbdo7ABTUAlPSLR5ZZIk5lR1ANxRmkKJiXQhAIwBW0ALyyY86KAhXrcJGiy5kBOQ5LlKNekw-yrAES0pbW4dCKyqo0mvbQ2oxxuvoA5uY2YAiIAJ7obnS5+ULsYgA8AM6IZALcqQB8jLDA0OLsUnHALdAAegD8UCmIBiYA4hnittl5BUXIJSPGAHTsldW1DU2srUbL7VJLhrUAJuK7HVBd8v1AA

    Though noted above in #27387 (comment), it doesn't work in the case of overloads.

    Reacted by Tomasz Lenarcik and Max Duval
  25. apendua commented on Aug 22, 2024

    @apendua
    Tomasz Lenarcik (apendua)
    on Aug 22, 2024
    Contributor
    More actions

    Daniel Rosenwasser (@DanielRosenwasser)

    Though noted above in #27387 (comment), it doesn't work in the case of overloads.

    Good point! Btw, the reason this currently does not work:

    const fileInput =
      /** @type {typeof document.querySelector<HTMLInputElement>} */
      (document.querySelector)("input[type=file]")

    is already pointed out in the other issue, here:

    #51694

    Do you think there's a chance the other PR will be merged? i.e. #51695

  26. tif-calin commented on Oct 14, 2024

    @tif-calin
    Culi T (tif-calin)
    on Oct 14, 2024
    More actions

    I'm a big TypeScript fan that's trying out a new project where I use JSDocs instead of TS. Here are some of the learnings I've been forced to do along the way.

    Passing in a generic slot to a function

    The key here is utilizing ReturnType.

    TypeScript version:

    const patchReq = async <T>(path: string, data: Partial<T>): T => makeRequest('PATCH', path, data);
    
    const updateBallot = async (ballot: Partial<Ballot>) => patchReq<Ballot>(`elections/${ballot.id}`, ballot);
    //      ^? returns Promise<Ballot>

    JSDocs version:

    /** @template T; @param {string} path; @param {Partial<T>} data; @returns {Promise<T>} */
    const patchReq = async (path, data) => makeRequest('PATCH', path, data);
    
    /** @param {Partial<Ballot>} ballot; @returns {ReturnType<typeof patchReq<Ballot>>} */
    const updateBallot = async (ballot) => patchReq(`ballots/${ballot.id}`, ballot);

    Warning

    As pointed out by Niki (@phaux) above, this often fails when there are overloads.

    React useState

    This was already mentioned above, but I think it's useful to rewrite. Typecasting the initial value has never failed me. Make sure to remeber the parentheses

    const [optionalTitle, setOptionalTitle] = useState(/** @type {null | string} */(null));
    //      ^? null | string

    Note

    I would prefer to have instead written on the line before the useState statement /** @type {ReturnType<typeof useState<null | string>>} */ but this also fails due to useState's overloads with undefined

  27. Petra Jaros (Peeja)
    added a commit that references this issue on Jan 15, 2025

    Tweaks to work better in practice with `freeway`

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: JSDocRelates to JSDoc parsing and type generationRelates to JSDoc parsing and type generationDomain: JavaScriptThe issue relates to JavaScript specificallyThe issue relates to JavaScript specificallyIn DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScriptcheckJsRelates to checking JavaScript using TypeScriptRelates to checking JavaScript using TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.