Warning: A lot of this page assumes you are using VSCode as your IDE, which is important to note because not all IDE's handle JSDoc the same way. I also focus on how JSDoc can provide type-safe JS through VSCode's TypeScript engine.
Table of Contents
- Resources
Resources
What & Link | Type |
---|---|
Official JSDoc Docs | Official Docs |
devdocs.io/jsdoc | Searchable Docs |
Devhints Cheatsheet | Cheatsheet |
theyosh.nl Cheatsheet | Printable Cheatsheet |
Advanced Usage
Type Guards
This is pretty crazy, but you can actually implement a type-guard completely in JSDoc! After a bunch of digging, I found the right syntax thanks to this Github issue comment.
Syntax:
// Where YOUR_TYPE is understood type (either naturally inferred, or through JSDoc)
/**
* @param {any} value
* @return {value is YOUR_TYPE}
*/
function isYourType(value) {
let isType;
/**
* Do some kind of logical testing here
* - Always return a boolean
*/
return isType;
}
Full Example:
// @ts-check
/**
* @typedef {{model: string}} Robot
*/
/**
* @typedef {{name: string}} Person
*/
/**
* @param {object} obj
* @return {obj is Person}
*/
function isPerson(obj) {
return 'name' in obj
}
/**
* Say hello!
* @param {Robot | Person} personOrRobot
*/
function greet(personOrRobot) {
if (isPerson(personOrRobot)) {
// Intellisense suggests `.name`, but not `.model`
console.log(`Hello ${personOrRobot.name}!`);
// Below will throw type error
console.log(personOrRobot.model);
} else {
// Intellisense suggests `.model`, but not `.name`
console.log(`GREETINGS ${personOrRobot.model}.`);
}
}
Annotating destructured parameters
If you want to annotate the type of variables that are assigned via destructuring, it can be a little complicated with JSDoc.
For destructuring directly, it looks like this:
/**
* @typedef {object} DestructuredUser
* @property {string} userName
* @property {number} age
*/
/** @type {DestructuredUser} */
const {userName, age} = getUser();
For annotating on a function argument:
/**
* @param {object} obj
* @param {string} obj.userName
* @param {number} obj.age
*/
function logUser({userName, age}){
console.log(`User ${userName} is ${age} years old.`);
}
... Or with a separate typedef
/**
* @typedef {object} DestructuredUser
* @property {string} userName
* @property {number} age
*/
/** @param {DestructuredUser} param */
function logUser({userName, age}){
console.log(`User ${userName} is ${age} years old.`);
}
You can even annotate on arrow functions:
/**
* @param {object} obj
* @param {number} obj.total
* @param {string} obj.vendorName
*/
const printReceipt = ({total, vendorName}) => {
// total and vendorName will have correct types! :)
}
Annotating function signatures / function as a type
This looks pretty similar to TS - return type should come after arguments with colon:
/**
* @typedef {function(string): string} StringProcessor
*/
// It is far easier to write as a multi-line typedef with arg names
/**
* @typedef {function} StringProcessor
* @param {string} inputStr - String to transform
* @returns {string} Transformed string
Here are some practical examples:
/**
* @typedef {function(string): string} StringProcessor
*/
// Annotating single variable
/**
* @type {StringProcessor}
*/
const upperCaser = (input) => {
return input.toUpperCase();
}
// Within another typedef
/**
* @typedef {Object<string, StringProcessor>} StringProcessorsCollection
*/
/**
* @type {StringProcessorsCollection}
*/
const myStringMethods = {
lowerCase: function(input) {
return input.toLowerCase();
}
}
Classes and Constructors
In general, documenting ES2015 (ES6) classes and their associated parts (constructor, methods, etc.) is well-supported in JSDoc. You can find some great examples of how to do this in:
- The TS Handbook - JSDoc Reference: "Classes"
- jsdoc.app: ES2015 classes
- This S/O answer (43581677)
However, there are some additional notes I would like to add:
-
You can document a class outside of its implementation, however, this is incredibly complex with pure JSDoc, and I would strongly recommend just using a
.ts
file for this type of use-case.- This is hard to do in general, and there are not many good reasons to be doing it anyways
- This is an example of defining a constructor on an interface
-
Trying to document a class that is structured as an IIFE with JSDoc is... very difficult. In fact, I would recommend never even attempting to do so; I'm fairly certain there might be some open bugs that make this almost impossible
- This is especially true if you are also trying to generate declaration files files from your JSDoc; I kept running into a "Declaration emit for this file requires using private name" error
- I think most of the ways to get this to work require a lot of explicitly creating types outside of the IIFE, then importing those types in and merging them; lots of extra work
Casting and Type Coercion in JSDoc
At least in @ts-check
land, you probably won't have to deal with casting in JSDoc much. For example, usually annotating the type during assignment is enough to cast an any
to a specific type.
However, for casting unknown
or situations with inferred "no overlap", there are some other escape hatches.
Simple Casting
One way is to wrap with parenthesis during assignment:
/** @type {unknown} */
let huh;
/**
* Works!
*/
/** @type {number} */
const success = (huh);
/**
* FAILS
*/
/** @type {number} */
const failed = huh;
TIP: Many formatters remove "unnecessary" parenthesis wrapping (such as Prettier), but they will preserve it if you inline the JSDoc annotation with the code. Like so:
const success = /** @type {number} */ (huh);
Advanced Casting
If the above didn't work, you are probably in a "neither type sufficiently overlaps" situation. I actually couldn't find this documented anywhere, but this worked for me, although it is not pleasant to look at!:
/**
* @typedef {object} Person
* @property {string} name
* @property {number} age
*/
/** @type {number} */
let something;
/**
* Works!
*/
const success = /** @type {Person} */ (/** @type {unknown} */ (something))
/**
* FAILS
*/
/** @type {Person} */
const failed = (something);
WARNING: Linters might automatically remove the parenthesis around the inner-most cast, which breaks this. You should be able to disable the linter on that specific line by following your specific linter instructions.
For prettier:// prettier-ignore
disables for the next line.
WARNING: With the above solution, VSCode does not like trying to cast to the same variable (reassignment). Just create a new variable and copy into it while casting if you are trying to change the type of an existing one.
Generics in JSDoc
If you are a TypeScript user, you might already be wondering about generics. How can you use them with JSDoc?
Luckily, support for generics in JSDOC (with type-checking) landed in TS / VSCode a while ago.
For accepting generics, the syntax is actually easier than one might expect - the most important piece is to declare a generic with @template T
. You can then reuse that generic type T
, just like you can in TypeScript.
Here is a fully-functional example:
/**
* Takes any object with a name prop and removes it
* @template T
* @param {T & {name?: string}} inputObj
* @returns {Omit<T, 'name'>}
*/
const deleteName = (inputObj) => {
/** @type {typeof inputObj} */
const copy = JSON.parse(JSON.stringify(inputObj));
delete copy.name;
return copy;
}
// IDE will automatically infer type of T, and also infer the
// type of nameLess as {age: number, name: string} without `name`
const nameLess = deleteName({
age: 50,
name: 'Gregory'
});
âš Of all the parts of JSDoc, this is one that is especially different based on IDE; not all IDEs support it, and some support it differently.
Important links:
- Google Closure-Compiler: Generic Types
- TypeScript Repo: Issue #1178 (adding support)
Tuple Types
VSCode now supports Tuple types expressed through JSDoc, in a variety of different ways.
Standard Tuple Type:
/** @typedef {[string, number]} NameAgeTuple */
/** @type {NameAgeTuple} */
const Robert = ['Robert Smith', 45];
Literal Tuple Type (helpful for exhaustive array checks, etc.)
/** @typedef {['Espresso', 'Drip']} CoffeeLiteralTuple */
// Above could also be written as `{readonly ['Espresso', 'Drip']}`
/** @type {CoffeeLiteralTuple & string[]} */
const coffeeTypes = ['Espresso', 'Drip'];
coffeeTypes.push('Cold Brew');
Literal Tuple Types are especially helpful, because they can also be reused as unions:
/** @typedef {['Dog', 'Cat', 'Bird']} PetsAllowedInLeaseTuple */
/** @type {PetsAllowedInLeaseTuple} */
const AllowedPets = ['Dog', 'Cat', 'Bird'];
/**
* @param {PetsAllowedInLeaseTuple[number]} petType - Union pet type, extracted from tuple
* @param {string} name
*/
const addPetToLease = (petType, name) => {
//...
}
addPetToLease('Bird', 'Tweety');
Importing external / exported types directly in JSDoc
This is kind of a crazy feature but, at least in VSCode, you can actually import types from specific modules by using import
directly within a JSDoc block! For example, if we wanted to explicitly type a variable as NodeJS's "fs" -> "Stats" type, we could use:
/**
* @type {import('fs').Stats}
*/
let fsStats;
// Or, via typedef
/** @typedef {import('unist').Node} AstNode */
You can read a bit more about this feature (implemented in 2018) in various spots:
Importing Non-TS Code and/or CommonJS Module.Exports
If you try to use the above trick to import something that is not a type, and/or specifically part of a CommonJS module.export = {}
pattern, you might get a "namespace has no exported member" error. Example:
/**
* @typedef {typeof import('../my-js-file').myExportedFunc} MyFunc
*/
// ERROR: Namespace '"___/my-js-file".export=' has no exported member 'myExportedFunc'.ts(2694)
This appears to be a bug, and there is an open issue to track it (#29485), however, luckily in the meantime there is a workaround! - using bracket notation instead of dot notation.
Thanks to this comment, I have tried and verified that the following works:
// Use `import(FILE_PATH)['EXPORTED_THING']` instead of `import(FILE_PATH).EXPORTED_THING
/**
* @typedef {typeof import('../my-js-file')['myExportedFunc']} MyFunc
*/
Inline Code Examples
This is a feature of JSDoc that I think is pretty neat, since I tend to believe that documentation is more likely to be kept up-to-date the closer it lives to the source code, so having inline examples of how to use / call a function right next to its implementation is pretty cool.
The generic JSDoc tag to add an inline example of how to use the thing you are documenting is the @example
tag. However, VSCode implements an extended version, which supports multi-line examples + language specifiers (Markdown style).
The easiest way (IMHO) to format these is with fenced code blocks, using backticks, just how you would in Markdown. For example:
/**
* @typedef {'pinball' | 'skee-ball' | 'ddr' | 'whack-a-mole'} GameName
*/
/**
* Get an adjusted point total, based on game weight and house rules
* - Make sure to pass un-adjusted scores
* @example
* ```js
* const adjusted = getAdjustedTotal({
* ddr: 2500,
* "whack-a-mole": 10
* }).adjustedTotal;
* ```
* @param {Partial<Record<GameName, number>>} gameHistory
* @returns {{total: number, adjustedTotal: number, avgWeight: number}} totals
*/
function getAdjustedTotal(gameHistory) {
// implementation details omitted
}
VSCode - JavaScript Type Safety with JSDoc
Intro to Type Safety with JSDoc
VSCode has an awesome baked-in feature, which is that it can provide JS type-safety tooling, powered by JSDoc comments. This is built-in to VSCode and requires no extra tooling, transpiling, or even config files.
How to Trigger the JS Type Checker
Triggering the built in JS Type Checker options:
- Add
//@ts-check
to top of JS file -
Add setting to preferences:
-
"javascript.implicitProjectConfig.checkJs": true
- Can be added to either global or workspace
settings.json
- Can be added to either global or workspace
-
-
Add a
tsconfig.json
orjsconfig.json
file in the project to control the settings (and addcheckJs: true
)- If using
tsconfig.json
, also addallowJs: true
- If using
Although these built in options internally ("salsa" engine?) actually use TypeScript features to do the checking/linting (you can read more about that here), they use JSDoc to augment the interpretation/inferred types of JS.
Programmatically Scanning JS for Type Issues
You might notice that VSCode tends to only check and report type problems on JS files that are currently open, instead of every JS file in the workspace. To get around this, you have a few options:
-
Check files with the TypeScript compiler, directly
-
You can actually just run your JS through TSC, even without any TS or a
tsconfig
. UsenoEmit
to make sure no files are output.- Example:
tsc --noEmit --project jsconfig.json
- Example:
- This is also a way to use JSDoc for type-safety, even without VSCode! However, it requires that you install the TypeScript compiler globally.
- NOTE: Even if you run this command from within VSCode, it won't make the problems caught automatically show up in the VSCode problems panel.
-
-
Use VSCode tasks and
problemMatcher
option- If you want all problems to be reported to and displayed in VSCode, you are (currently) forced to create a custom task. This will let VSCode run the TSC command directly and use the
problemMatcher
option to help it understand how to interpret the CLI response. - See this S/O and this open issue thread for VSCode
- If you want all problems to be reported to and displayed in VSCode, you are (currently) forced to create a custom task. This will let VSCode run the TSC command directly and use the
- Try out the experimental
enableProjectDiagnostics
setting (see this S/O)
Simple Example:
WARNING: JSDoc TS annotations MUST be in a multi-line comment block (
/** ___ */
) comment to be be parsed. They are ignored in single line (//
) comments!!!
/**
* @type {google.maps.StreetViewPanorama}
*/
let pano = this.mapObjs.svPano;
/**
* @type Array<{localPath:string, fullPath: string}>
*/
let filePaths = [];
/**
* @typedef {"Hello" | "World"} MyStringOptions
*/
// You can even re-use types!
/**
* @typedef {object} Person
* @property {string} name - Person's preferred name
* @property {number} age - Age in years
* @property {boolean} likesCilantro - Whether they like cilantro
* @property {string} [nickname] - Optional: Nickname to use
*/
/**
* @type Person
*/
let phil = {
name: 'Phil Mill',
age: 25,
likesCilantro: true
}
/**
* @type Person
*/
let bob = {
name: 'Robert Smith',
age: 30,
likesCilantro: true,
nickname: 'Bob'
}
/**
*
* @param {Person} personObj
*/
function analyzePerson(personObj){
// Intellisense will fully know the type of personObj!
console.log(personObj.name);
}
VSCode - Advanced JS Type-Safety with JSDoc
Package Types from node_modules
And you can even install module type defs, like those from @types
. This works the same as in TypeScript, so see my notes about "including external type definition files" for details. For most JS projects, since you are likely missing a tsconfig.json
file, the easiest option might be a one-off triple-slash directive usage.
For pulling in types from a module, one at a time, you can use the import syntax within JSDoc. I already have a section on this elsewhere on this page.
This is an extremely helpful section in the VSCode docs: "Typings and Automatic Type Acquisition"
JSDoc Globals in VSCode
Unfortunately, for globals, it looks like JSDoc or inline comments are not supported (issue #15626, doc change) the only way to tell VSCode about those is through a TypeScript "ambient declaration file" (e.g. *.d.ts
file). For example, you can create a globals.d.ts
file, declare your globals / interfaces / etc., and VSCode's type checker will pick up on it, despite your project being JS code.
You can also change how you declare them; explicitly creating via global.{yourVarName}
(or globalThis
) will suppress cannot find name {yourVarName}
errors, and the IDE should even preserve the type if the variable is read in a different file.
Scripts vs Modules, Block-Scoped Variables
The default behavior of TS is that a file is a module if it imports or exports anything, otherwise it is a script. And, if it is a script, due to the lack of closure, top level variables become global. Sometimes this is accurate, for example in this setup:
<!-- Files:
- index.html
- file_A.js
- file_B.js
-->
<!-- index.html -->
<script src="./file_A.js"></script>
<script src="./file_B.js"></script>
If both files A and B are scripts, not modules, and both have a top level variable declared as const myStr = 'hello';
, you will get an error:
Cannot redeclare block-scoped variable 'myStr'.ts(2451)
Again, in the above scenario, this makes sense; VSCode and TS are correct. If we wanted to force these files to be isolated from each other, while still running in index.html
, we could:
- Change the script tags to use
<script type="module">
for ES6 module encapsulation (caniuse)
However, this actually don't tell VSCode to interpret the files any differently, and the error wont go away. Furthermore, VSCode will continue to throw the error even if the files are scripts, but won't conflict due to directory separation. For example:
- jsconfig.json
- dir_A/
- index.html
- file_A.js
- dir_B/
- index.html
- file_B.js
Even if file_A
is only used in dir_A/index.html
, and file_B
is only used in dir_B/index.html
, TSC will still complain about any top-level variables that exist in both files and it thinks will conflict (they won't). How do we explicitly tell TSC about this?
-
If we want to force these files to act like modules, we can add an empty export declaration:
-
Syntax
- ESM:
export {}
- CJS:
module.exports = {}
- ESM:
- In HTML, you also need to update script tags to use
<script type="module">
when importing - This also prevents variables from becoming globals, which might be something that you actually don't want to lose.
-
-
For our exact scenario, with pure JS in a web environment, our options are:
-
Move config files down
- Add:
/dir_A/jsconfig.json
- Add:
/dir_B/jsconfig.json
- Delete:
/jsconfig.json
- Reload your workspace and/or TS engine
- Add:
-
Give top-level variables closure (e.g. by wrapping in an IIFE)
// Change this... const myStr = 'hello' console.log(myStr); // ...to this: (() => { const myStr = 'hello'; console.log(myStr); })();
-
Change variable type to something that is not block-scoped
- Change
const
orlet
tovar
- Change
-
Also, although you might think it would work, turning off / deleting all config files and then individually adding the
// @ts-check
directive to each file does not fix it
I've asked for a better solution to this problem on StackOverflow. If you know of a better approach, you should respond there, so you can claim some credit!
How to ignore errors in multi-line blocks (e.g JSX)
You basically can't, at least not at the moment - see:
- https://github.com/microsoft/TypeScript/issues/31147
- https://github.com/Microsoft/TypeScript/issues/19573
VSCode Type-Safety JSDoc - Issues
If you run into issues with this, here are some things to check or try:
-
"
cannot find name ___ ts(2304)
"-
VSCode seems to have an issue with ambient declaration files (
*.d.ts
) being picked up without a config file. Any of the following should work:-
Add a
tsconfig.json
file (orjsconfig.json
, as a fast alternative)- Bare-bones fix: add
jsconfig.json
with contents of{}
or something like{"exclude": ["node_modules"]}
- Bare-bones fix: add
- Use a triple slash directive at the top of the file, for example:
/// <reference path="../types.d.ts"/>
or/// <reference types="googlemaps"/>
- Keep the ambient type file open while editing (right click tab, "Keep Open") - quick hack
-
- Can you add an import that brings in the namespace'd type?
- Is the type declared under a namespace or module that doesn't match where you are trying to use the type?
- If you have used
import
orexport
in a TS file that you are trying to use as an ambient type declaration file, make sure types are wrapped withdeclare global {}
. - If using a
tsconfig.json
file, usetsc --listFiles --p {pathToTsConfigJson}
to help see what files are getting included (kudos)
-
-
Type assigned to CommonJS (require()) variable is
typeof import()
, and you aren't getting the correct type that corresponds with the exported variable- Instead of
const myImport = require('my-lib');
tryconst myImport = require('my-lib').default;
- The above is actually a TS / CommonJS interop problem, not a JSDoc problem
- Instead of
-
Using a
tsconfig.json
for checking JS doesn't seem to be working / rules are not enforced- Do you have both
allowJs
andcheckJs
on? -
Check for outdated / mismatched TypeScript versions. You can force VSCode to use a newer, or specific version of TS, by following these steps
- You can always install
typescript
as a devDependency, and then usesettings.json
to tell VSCode to use it as the active version
- You can always install
-
Do you accidentally have a TS file with the same filename (omitting extension) in the same folder? E.g.
src/helpers.ts
andsrc/helpers.js
?- If this is the case, TSC will ignore the
.js
file, even if you explicitly add the file toinclude
and havenoEmit: true
(you would think noEmit would do the trick, but no).
- If this is the case, TSC will ignore the
- As a last resort, if you really don't need full TS support, try just using a
jsconfig.json
config instead (which supports most of the same options)
- Do you have both
Important reference material
-
JSDoc pages (jsdoc.app)
- devdocs.io/jsdoc
-
Some guides:
-
TruckJS / rbiggs's guide on Medium
- Excellent guide on advanced usage of JSDoc
- @rstacruz's guide
- Gleb Bahmutov: Trying TypeScript
- CSS-Tricks: TypeScript Minus TypeScript
-
- JSDoc Cheatsheet - devhints.io
-
TypeScriptLang Docs:
- Intro to JS Type-Checking with TS
- Type Checking JavaScript Files
- JSDoc Reference / Supported JSDoc Types
- Includes short examples
-
The TypeScript Github Repo
- Wiki Page: Type-Checking JavaScript Files
- Some of this Google closure cheatsheet applies...
Further reading
Here are some more guides on trying to get the benefits of TypeScript in JS (type-safety):
- https://austingil.com/typescript-the-easy-way/
- https://devblogs.microsoft.com/typescript/how-to-upgrade-to-typescript-without-anybody-noticing-part-1/
- https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7a2a63209b76
- https://github.com/Microsoft/TypeScript/wiki/JavaScript-Language-Service-in-Visual-Studio#JsDoc
- http://jonathancreamer.com/why-would-you-not-use-typescript/
- https://fettblog.eu/typescript-jsdoc-superpowers/
- https://ricostacruz.com/til/typescript-jsdoc
- https://github.com/Raynos/tsdocstandard
Generating Files From JSDoc
Some important links:
-
TypeScript Handbook
- My notes 👉 Building an NPM Package: Exporting Types
Emitting Type Definitions / Declarations from JSDoc Types
The main benefit of using JSDoc for type checking is really just the improvement of the IDE experience; unlike traditional TypeScript, you are not actually compiling your code.
However, if you are building a library, such as an NPM package, you might actually want to generate some output files; mainly a type declaration file.
This page from the TS Handbook covers how to setup a JSDoc project to output declaration files, but I'll also summarize below:
If you want to emit a types file based on JSDoc, the main thing you need to do is:
-
Make sure you are using a
tsconfig.json
file-
If you are using a
jsconfig.json
file, you will probably want to switch, or create an additional specialtsconfig.{name}.json
file just for emitting (such astsconfig.emit.json
)- You can use
jsconfig.json
files with TSC, but there are some related issues you might run into
- You can use
-
At a bare minimum the config needs to have:
{ "compilerOptions": { "allowJs": true, "declaration": true, "noEmit": false } }
-
You probably also want:
"module": "commonjs"
"moduleResolution": "node"
"esModuleInterop": true
-
You can use
"emitDeclarationOnly": true
if you don't want to copy the JS files and/or modify them in any way for distribution- If you do want TSC to copy the JS files over, it can even handle some basic transpiling for you! Use the
compilerOptions.target
to specify the desired output ES version.
- If you do want TSC to copy the JS files over, it can even handle some basic transpiling for you! Use the
-
Additionally, use
compilerOptions.outDir
orcompilerOptions.outFile
to control the location and/or name of the declaration file(s)- If you want a single emitted file, use
outFile
(but this be warned this is incompatible withcommonjs
)
- If you want a single emitted file, use
-
- Add
typescript
as a dev dependency, so everyone can usetsc
-
Call
tsc
with the config, which will create the file- If you have a special named config, pass via the
--project
flag. tsc --project tsconfig.emit.json
- If you have a special named config, pass via the
- Add the (final) generated
*.d.ts
file (or directory that contains the generated files) to yourpackage.json
, under thetypes
field
Be very careful using
*.d.ts
files in your source code, especially if you are relying on generated declaration files. Ambient declaration files (*.d.ts
) are essentially used as "guides" to the compiler; they are not considered for output file generation and do not emit any JS.
You also need version >= 3.7 of TypeScript in order for this to work (Ref A, Ref B), Ref C
There are alternative generators other than the TypeScript compiler, such as tsd-jsdoc, but TSC is recommended if it can work for your situation.
Where to Output JSDoc Generated TS Declaration Files
When using TSC to produce *.d.ts
files from JS source code, you have a few options on where to place those files. The common options seem to be:
-
Don't transpile JS (
emitDeclarationOnly: true
), and emit to/types
(viaoutDir
)- You can optionally use
declarationDir
instead ofoutDir
to control where types are emitted
- You can optionally use
- Transpile JS, and emit JS + TSD to
/dist
(viaoutDir
) -
Don't transpile JS, and don't emit to a separate folder; emit alongside JS files: this is basically the default config
-
Negative: this complicates organizing
src code
vspublished code
- Many people argue that generated files should not be in version control; if you follow this advice, you would need to carefully gitignore the specific generated *.d.ts files that are among your source files, but also make sure they are published to NPM
-
Emitting a Single Declaration File
In general, bundling all the emitted types into a single declaration file is not really necessary or possible with TS repos (see my disclaimer in my package authoring notes), but for generating from JSDoc, you can easily accomplish this with emitDeclarationOnly: true
, and the outFile
option.
Another approach - the tsd-jsdoc library is oriented towards this goal. Here is a guide on using it.
Caveats for Using JSDoc to Generate Declaration Files
The biggest caveat with using JSDoc to generate declaration files (especially for a NPM package entry point) is that (currently) it feels like an "all-or-nothing" approach. There is a limited set of controls to alter the behavior of the generated files (to be fair, JSDoc is not really the main purpose of TSC 😂), and once the files are generated, they aren't really meant to be hand-edited.
So, you usually see one of a couple options:
- Authors hand-edit the TS declaration files that are included in distribution, and also check them into VC (example). TSC is not used for generation.
- Authors use TSC for declaration generation, omit the files from VC (e.g. in
/dist
), and don't modify the generated output -
Authors use TSC for declaration generation, but include the generated (vendored) declaration files in VC
- This is usually combined with
outFile
to force a single declaration file, and having it created alongside the rest of the source files - Although this is checked into VC, not wise to hand-edit after generation, since changes will be wiped out by future generations
- This is usually combined with
-
Authors only use TSC to generate the initial set of types, and then hand-edit them afterwards
- I would not recommend this approach. I don't consider it a good idea to maintain duplicate type definitions (inline via JSDoc, and the declaration files). Plus, running TSC will overwrite your changes. This speaks to the "all or nothing" problem...
-
Authors merge generated declaration files with hand-coded types
- As a build step, you can always copy over hand-coded declaration files that can supplement the generated files from TSC
- This is somewhat common, especially for
*.d.ts
files, since those are not bundled with TSC's generated output
How To Export Types from Index Entry Point
If you are using TSC to generate declaration files from pure JavaScript with JSDoc, you might run into the funny question of how to export types (not implementations) from the entry-point (e.g. index.js
) of your library / app.
Normally, in a TypeScript project, you can easily export types just the same as you can export functions, classes, etc. You might even do something like export * from './my-types'
, where you import and then re-export types that your consumers will need, from within the entry-point.
But, in JS files, types only exist in JSDoc comments; how do you export from JS-land?
Options:
=== Export types one-by-one, with @typedef
blocks ===
/**
* @typedef {import('./my-types').Dog} Dog
*/
/**
* @typedef {import('./my-types').Cat} Cat
*/
Even if you never use the imported type within the JS file you declare it in, TSC will export them in the generated *.d.ts
file!
=== Export types in bulk ===
Currently not supported (AFAIK, and backed up by this comment by TruckJS) 😢
Ideally, this would work the same way it does in TypeScript, where you could write something like:
// These are NOT supported, and are merely for illustrative purposes
/**
* @typedef {import * from 'my-types'} MyTypes
* - Or maybe,
* @typedef {import('my-types') as MyTypes}
* - Or,
* @export {import('my-types')}
*/
None of these are supported, but some of the syntax has been discussed / proposed on this thread, and here. Also related is #14377.
=== Manual Declaration Editing ===
Of course, if you are hand-editing the declaration files that ship with your JS, you can add whatever you want.
You could also automate declaration editing; you could theoretically generate the bulk of the files with TSC, and then append the types you want exported (in bulk) with a simple build script.
Issues
Issue with Number Properties
Using a number as an object key is completely permissable in JavaScript, so long as you use bracket notation for accessing. For example:
const levels = {};
levels[0] = 'low';
levels[1] = 'medium';
levels[2] = 'high';
console.log(levels[0]);
// > 'low'
// You can also access by string
console.log(levels['0']);
// > 'low'
However, if you try to document this with JSDoc, you might run into an issue...
/**
* @typedef {object} Levels
* @property {string} 0
* @property {string} 1
* @property {string} 2
*/
// ERROR: TS: Identifier expected.
It looks like VSCode's integration with JSDoc for type-checking does not like anything other than a standard a-Z
character as the leading property key.
Number Property Key Workaround
- One workaround is to simply define the entire object within the first typedef line:
/**
* @typedef {{0: string, 1: string, 2: string}} Levels
*/
// No errors! :)
- Another workaround would be to use an actual TS file, e.g.
types.d.ts
, and follow my directions on having VSCode pick up on it.