Shruggie
Shruggie

Reputation: 938

JSDoc for reused Function interface

I'm connecting to multiple email tools and abstracting their APIs to one common sendEmail function with the same params and same returns for each service. That means that for every email service (Mailchimp, SendGrid...), I have to write a function which has an identical JSDoc describing the same @params and same @returns...

Is there a valid JSDoc syntax to use @typedef or similar with a Function, where instead of declaring @params and @returns above each function, just describe the type?

...Bonus points for not disabling ESLint's require-jsdoc

Upvotes: 7

Views: 3678

Answers (2)

cristian
cristian

Reputation: 1009

It's near the end of 2022, and I believe this question needs an updated answer. I cannot confirm this solution for code editors oher than VS Code, but if they support [email protected], they should be fine.

The solution uses @callback definitions, and TypeScript's import(...) statements (which, AFAIK, is only supported in JSDoc@3, thus, the version requirement).
Also, JSDoc@3 apparently does not interpret @type declarations as the return type, instead, it applies the declaration to the entire statement that follows - thus a function will inherit the @callback declaration for itself.

Solution

Define function signature

Create a @callback type specifying everything you need. In the example below, I am writing a graphql-yoga resolver function definition.

/** @typedef {import("graphql").GraphQLResolveInfo} GraphQLResolveInfo */
/** @typedef {import("@graphql-yoga/node").YogaInitialContext} YogaInitialContext */

/**
 * @callback GQLResolver
 * @param {TObj} obj
 * @param {TArgs} args
 * @param {YogaInitialContext} context
 * @param {GraphQLResolveInfo} info
 * @template TObj
 * @template TArgs
 * @template TReturn
 * @returns {Promise<TReturn>}
 */

Reuse - in the same file

To reuse the type, simply apply it with a @type declaration:

/** @type {GQLResolver< {}, { id: string }, boolean >} */
function someResolver(obj, args, context, info){}
// (obj: {}, args: { id: string }, context: YogaInitialContext, info: GraphQLResolveInfo) => Promise<boolean>

// You can also override the signature with additional `@param` and `@return` declarations
/**
 * @type {GQLResolver< {}, { id: string }, boolean >}
 * @param {{ otherId: number }} args
 * @param {{ something: string }} info
 * @return {Promise<{ success: boolean, errors: Array<string>}>}
 */
function someOtherResolver(obj, args, context, info) {}
// someOtherResolver = (
//    obj: {},
//    args: { otherId: number },
//    context: YogaInitialContext,
//    info: { something: string }
// ) => Promise<{ success: boolean, errors: Array<string> }>

Reuse types from an external files without @template parameters

Assuming that the @callback GQLResolver definition is in an external file types.js, make sure that the file exports at least an empty object - otherwise, it will not work. I don't know the exact reason, but it might be that JS/TS bails out when no symbols are exported.

// ./src/types.js

/**
 * @callback GQLResolver
 * ...
 */

export {} // <-- at least this

Then, use a TS-like import(...).TypeName declaration:

/** @typedef {import("types.js").GQLResolver} GQLResolver */

/** @type {GQLResolver} **/
function example(){}

NOTE:
If you have a JS file dedicated to typedefs like the above, please note that you do not need to import it as JS source in your project! The typedef {import(...)} works standalone, as long as the specified import exports any symobls.

Reuse from an external file with @template parameters

Importing types from external files, that make use of @template parameters, is possible, but it will not work as expected:

/** @typedef { import("types.js").GQLResolver } GQLResolver */
                                  ^^^^^^^^^^^   ^^^^^^^^^^^
                                 <GQLResolver>     <any>

The import misses the @template declarations, and considers the type invalid - thus, assigns type any.
The workaround is to specify template parameters near the imported symbol name:

/**
 * @typedef {import("types.js").GQLResolver<
 *   any,
 *   { id: string }
 *   boolean
 * }>} SomeSpecificGQLResolver
 */
// SomeSpecificGQLResolver = (obj: {}, args: { id: string }, context: YogaInitialContext, info: GraphQLResolveInfo) => Promise<boolean>

/** @type {SomeSpecificGQLResolver} */
function someResolver(obj, args, context, info){}
// someResolver = same as SomeSpecificGQLResolver

Upvotes: 2

Josh Coady
Josh Coady

Reputation: 2189

There is a way to define it. Use @callback which is the equivalent of @typedef for functions.

/**
 * @callback sendEmail
 * @param {string} to
 * @param {string} body
 * @returns {boolean} to indicate success or failure
 */

You can then use sendEmail as a type:

/**
 * @param {sendEmail} sender - function to send the email
 */
function createEmailService(sender) { ... }

The problem is there is not a good way to specify the type of a function because @type on a function is interpreted to be the return type of the function, not the entire function definition. So something like the following does not work.

/**
 * @type {sendEmail}
 */
function mailchimpEmailSender(to, body) { ... }

You can get it to work with the following, but it is not a great solution. I am still looking for a better solution.

/**
 * @type {sendEmail}
 */
let mailchimpEmailSender
mailchimpEmailSender = function(to, body) { ... }

Update: I have figured out that if you wrap the function declaration in parens it seems to allow the @type to apply to the variable instead of the function declaration.

/**
 * @type {sendEmail}
 */
const mailchimpEmailSender = (function(to, body) { ... })

This is the solution I like best thus far as it allows you to use the appropriate variable declaration keyword. The only downside is it requires you to remember to add in code that is not strictly necessary.

Upvotes: 6

Related Questions