Importing JSDoc type definitions
February 1, 2024 • JavaScript
I’m a big, big fan of using JSDoc docblocks to annotate JavaScript, primarily functions and class methods. If you’re not familiar, it’s a comment-based markup that looks like this:
/**
* Generic query for fetching an entry by type and id. Primarily for use by
* serverless preview function.
*
* @param {"page"|"post"} type
* @param {string} id
* @return {Object}
*/
The consistent, structured format makes it easy to document how a given function is intended to work: what it expects or supports, and what I can expect it to return.
Even though I’ve never once used it to generate documentation, I find it helpful for a variety of reasons:
- I often sketch out how a function will behave by writing out its docblock
- I find it often makes reading someone else’s code easier for me to understand
- It always makes reading my own code easier for me to understand 😅
Most importantly on a day-to-day basis, since TypeScript speaks JSDoc, text editors like Nova or VSCode can surface tooltips and autocompletion suggestions using docblock annotations in regular old JavaScript files:
This is rad!
But when function parameters have even moderately complex structures, it can get tedious and error-prone to define nested properties in a docblock, especially when that parameter type is used in multiple places around a project.
After recently spending some time on a few TypeScript-heavy projects, I’ve found myself missing the ability to have Interface-like definitions available for import across JavaScript-based projects.
I’ve had my eye on JSDoc’s @typedef tags for a while since they seem to serve this exact purpose:
/**
* @typedef {Object} Page
* @property {string} slug
* @property {string} title
* @property {string} url
*/
but since JSDoc annotations are just comment blocks, I’ve never understood how they’re meant to be written once and imported wherever they’re needed.
As it turns out, some kind souls on StackOverflow shared the correct incantation (back in 2018!), and it’s pretty straightforward.
The Recipe
First, compose a @typedef block somewhere in your project. I like the pattern of keeping these in a central location, like src/types.js:
// src/types.js
/**
* @typedef {Object} Page
* @property {string} slug
* @property {string} title
* @property {string} url
*/
module.exports = {};
Be sure to export something from this file — this seems to be a requirement for TypeScript to find the @typedef blocks.
Next, import the file that contains your @typedef wherever it’s needed:
// src/utils/buildPermalink.js
const Types = require("../types");
Finally, to refer to any type definition defined in the imported file, use Types.<namepath>:
/**
* Build permalinks for pages and posts
* @param {Types.Page | Types.Post} entry
* @return {}
*/
function buildPermalink(entry) {
And that’s it! Now your TypeScript-savvy editor will show tooltips and offer autocomplete suggestions for that sweet, sweet DX we all crave.
Housekeeping
- This also works for ESM imports ✨
- If you have linting rules enabled to warn or error on unused variables, you can disable it like this:
// src/utils/buildPermalink.js
/* eslint-disable-next-line no-unused-vars */
const Types = require("../types");
- You can name the variable anything you want, but
Typessuggested on StackOverflow makes sense to me