JSDoc TypeScript utils
A plugin with utilities to make your TypeScript-like docs JSDoc valid
Introduction
This plugin allows you to take advantage of the TypeScript language server on modern IDEs/editors to generate intelligent code completion using your JSDoc comments, and at the same time, be able to generate a documentation site with JSDoc CLI.
The reason this plugin exists is that JSDoc comments as specified by its convention are not 100% compatible with TypeScript, and they don't cover all the cases; sometimes, writing something that is JSDoc valid can end up killing the code completion, and other times, writing something for the code completion can cause invalid code for the JSDoc CLI.
The plugin counts with a few (toggleable) features you can use to write code valid for TypeScript that will also be valid for the JSDoc CLI.
There are also a few features that are designed to make the code compatible with the ESLint plugin for JSDoc, highly recommended if you are getting serious with JSDoc.
Configuration
The first thing you need to do after installing the package, is to add it to the plugins
array on your JSDoc configuration:
{
// ...
"plugins": [
"jsdoc-ts-utils",
// ...
]
}
It's important do add it first as it makes modifications to the code in order to make it valid. If a plugin that requires the code to be valid gets executed first, you may end up with unexpected errors.
Since JSDoc doesn't allow to add configuration options on the plugins
list, if you need to change the settings, you'll need to create a tsUtils
object:
{
// ...
"plugins": [
"jsdoc-ts-utils",
// ...
],
"tsUtils": {
"typedefImports": true,
"typeOfTypes": true,
"extendTypes": true,
"modulesOnMemberOf": true,
"modulesTypesShortName": true,
"parentTag": true,
"removeTaggedBlocks": true,
"removeTags": true,
"typeScriptUtilityTypes": true,
"tagsReplacement": {}
}
}
Option | Default | Description |
---|---|---|
typedefImports |
true |
Whether or not to enable the feature that removes typedef statements that use import . |
typeOfTypes |
true |
Whether or not to enable the feature that replaces {typeof T} with {Class.<T>} . |
extendTypes |
true |
Whether or not to enable the feature that allows intersections to be reformatted. |
modulesOnMemberOf |
true |
Whether or not to enable the feature that fixes modules' paths on memeberof so they can use dot notation. |
modulesTypesShortName |
true |
Whether or not to register modules types without the module path too. |
parentTag |
true |
Whether or not to transform all parent tags into memberof . |
removeTaggedBlocks |
true |
Whether or not to remove all blocks that have a @jsdoc-remove tag. |
removeTags |
true |
Whether or not to remove all tags that follow a @jsdoc-remove-next-tag tag. |
typeScriptUtilityTypes |
true |
Whether or not to add the external utility types from TypeScript. |
tagsReplacement |
null |
A dictionary of tags to replace, they keys are the tags being used and the values the tag that should be used. |
Features
Import type defintions
/**
* @typedef {import('./daughters').Rosario} Rosario
*/
This syntax can be used to import a type from another file without having to import a variable that you won't be using. It not only allows you to import the type of an existing object, but also a type defined with @typedef
.
The feature will detect the block and replace it with empty lines (so it doesn't mess up the lines of other definitions on the generated site).
In case you want to import the type but show it as an external on the site, because it's the type of an installed library and you want to reference it or something, you could use the following syntax:
/**
* @typedef {import('family/daughters').Pilar} Pilar
* @external Pilar
* @see https://npmjs.com/package/family
*/
The feature will only replace the line for the @typedef
and leave the rest.
This is enabled by default but you can disable it with the typedefImports
option.
Use typeof
as a type
/**
* @typedef {typeof Rosario} ClassRosario
*/
One of the most "complicated" things you'll find when typing with JSDoc is how to type class constructors. Let's say a function receives a parameter that is not an instance of the class but its constructor, the @param
can't be the type of the class: you won't get the autocomplete if you call new
on it.
Using the previous feature you can define a @typedef
with an import
to the file and the brackets syntax (import('...')['MyClass']
) to get the constructor reference... but what if you are on the same file as the class? that's when you use {typeof MyClass}
.
The typeof Class
inside a type is not valid JSDoc, so this feature will transform it in order to use the convention Class.<MyClass>
:
/**
* @typedef {Class.<Rosario>} ClassRosario
*/
This is enabled by default but you can disable it with the typeOfTypes
option.
Extending existing types
/**
* @typedef {Object} Entity
* @property {string} shape ...
* @property {string} name ...
*/
/**
* @typedef {Entity & PersonProperties} Person
*/
/**
* @typedef {Object} PersonProperties
* @property {number} age ...
* @property {number} height ...
* @augments Person
*/
You can extend an existing type by defining a new one with the new attributes/properties and another one that intersect it with the original.
The feature will find the one with the intersection, look if there's a type that aguments
/extends
it, remove the intersection, move the attributes/properties to its own definition and remove the extra definition:
/**
* @typedef {Object} Entity
* @property {string} shape ...
* @property {string} name ...
*/
/**
* @typedef {Entity} Person
* @property {number} age ...
* @property {number} height ...
*/
If the feature can't find a type the aguments
/extends
the intersection, it will simply transform it into a union.
Note: You should always define the attributes/properties type after the intersection type, so when the feature removes it, it won't mess up the lines of other definitions on the generated site.
This is enabled by default but you can disable it with the extendTypes
option.
Modules' paths on @memberof
/**
* @typedef {Object} Entity
* @property {string} shape ...
* @property {string} name ...
* @memberof module.services.utils
*/
This is meant to solve issues with the ESLint plugin: If the plugin is configured for TypeScript, you can't use the module:
prefix on @memberof
, as the parser doesn't support it.
Adding modules to definitions is something really useful to group parts of your project on the generated site, so this feature allows you to use dot notation: module.[path]
instead of module:[path]
and it will automatically transform it before the JSDoc CLI reads it.
This is enabled by default but you can disable it with the modulesOnMemberOf
option.
Modules' types short names
/**
* @typedef {Object} Entity
* @property {string} shape ...
* @property {string} name ...
* @memberof module.services.utils
*/
/**
* @param {Entity} entity
* ...
*/
When you add @memberof
to a type definition, you cannot longer reference the type by its name alone, you have to use the module:[path].[type]
format for the JSDoc CLI to properly link it... not great.
This features intercepts the creation of the links for types on the generated site and if the type has the module:
prefix, it will also register it without the prefix as an alias.
Something similar happens with externals; when you want to reference an external, you need to use externa:[type]
... yes, the feature takes care of that too.
This is enabled by default but you can disable it with the modulesTypesShortName
option.
@parent tag
/**
* @typedef {Object} Entity
* @property {string} shape ...
* @property {string} name ...
* @parent module:services/utils
*/
If you use special characters on your modules names, like /
, then modulesOnMemberOf
won't be enough to help you: the parser the ESLint plugin uses for TypeScript only allows dot notation.
This feature is simply an alias for @memberof
: you put whatever you want in the @parent
tag, and before generating the site, it will be converted to @memberof
.
If you are taking advantage of this feature and using the ESLint plugin, you should add
parent
to thedefinedTags
option of thejsdoc/check-tag-names
rule.
This is enabled by default but you can disable it with the parentTag
option.
Remove blocks
/**
* @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess
* @jsdoc-remove
*/
Sometimes you have types that could make the site generation fail, and you can't fix them with any of the other features this plugin provides. For those cases, you can use the @jsdoc-remove
tag to completely remove the block before it even gets processed by the JSDoc CLI.
This is enabled by default but you can disable it with the removeTaggedBlocks
option.
Remove tags
/**
* @jsdoc-remove-next-tag
* @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess
* @external JQueryOnSuccess
* @see {@link http://api.jquery.com/jQuery.ajax/#success}
*/
While the jsdoc-remove
tag may be util to get rid of those blocks that breaks the site generation, sometime you just want to remove one single tag, as separating in multiple blocks may make the code hard to read. For those cases, you can use the @jsdoc-remove-next-tag
tag, and it will only remove the next tag.
You can even use it multiple times in a single block:
/**
* @jsdoc-remove-next-tag
* @typedef {JQuery.AjaxSettings['success']} JQueryOnSuccess
* @external JQueryOnSuccess
* @see {@link http://api.jquery.com/jQuery.ajax/#success}
* @jsdoc-remove-next-tag
* @typedef {JQuery.AjaxSettings['error']} JQueryOnError
* @external JQueryOnError
* @see {@link http://api.jquery.com/jQuery.ajax/#error}
*/
This is enabled by default but you can disable it with the removeTags
option.
TypeScript utility types
/**
* @typedef {Object} Entity
* @property {string} shape ...
* @property {string} name ...
*/
/**
* @param {Partial<Entity>} entity
* ...
*/
TypeScript already comes with a set of utility types that you can use on your code and that the code completion will understand.
This feature basically takes those types and define them as @external
s, so they can have links on the generated site.
This is enabled by default but you can disable it with the typeScriptUtilityTypes
option.
Tags replacement
/**
* @parametro {string} name
* @parametro {number} age
* @retorno {Entity}
*/
This feature doesn't have a specific use case, it was built for the @parent
tag and I decided to expose as maybe someone would have the need for it.
The feature allows you to replace tags before generating the site. You define a "replacement dictionary" on the plugin configuration:
{
// ...
"plugins": [
"jsdoc-ts-utils",
// ...
],
"tsUtils": {
"tagsReplacement": {
"parametro": "param",
"retorno": "returns"
}
}
}
And before generating the site, the feature will replace the tags from the keys with the ones from the values.
No modification to the tagsReplacement
option will affect the @parent
tag feature, they use different instances.
Development
Scripts
Task | Description |
---|---|
docs |
Generates the project documentation. |
lint |
Lints the staged files. |
lint:all |
Lints the entire project code. |
test |
Runs the project unit tests. |
todo |
Lists all the pending to-do's. |
Repository hooks
I use husky
to automatically install the repository hooks so the code will be tested and linted before any commit, and the dependencies updated after every merge.
Commits convention
I use conventional commits with commitlint
in order to support semantic releases. The one that sets it up is actually husky, that installs a script that runs commitlint
on the git commit
command.
The configuration is on the commitlint
property of the package.json
.
Releases
I use semantic-release
and a GitHub action to automatically release on NPM everything that gets merged to main.
The configuration for semantic-release
is on ./releaserc
and the workflow for the release is on ./.github/workflow/release.yml
.
Testing
I use Jest to test the project.
The configuration file is on ./.jestrc.js
, the tests are on ./tests
and the script that runs it is on ./utils/scripts/test
.
Linting && Formatting
I use ESlint with my own custom configuration to validate all the JS code. The configuration file for the project code is on ./.eslintrc
and the one for the tests is on ./tests/.eslintrc
. There's also an ./.eslintignore
to exclude some files on the process. The script that runs it is on ./utils/scripts/lint-all
.
For formatting I use Prettier with my custom configuration. The configuration file for the project code is on ./.prettierrc
.
Documentation
I use JSDoc to generate an HTML documentation site for the project.
The configuration file is on ./.jsdoc.js
and the script that runs it is on ./utils/scripts/docs
.
To-Dos
I use @todo
comments to write all the pending improvements and fixes, and Leasot to generate a report.
The script that runs it is on ./utils/scripts/todo
.