A few months ago, there was a something issue in the JavaScript ecosystem.
It was the migration of Svelte’s codebase from TypeScript to JavaScript.
Yes, it’s not a typo.
Svelte, during its version 3 to 4 upgrade, was rewritten in JavaScript, and the existing TypeScript code was pushed to the version-3
branch.
Despite significant concerns from the Svelte community about this decision made by Rich Harris and the Svelte team, two months have passed since the release of Svelte 4, and they have proven their choice to be right.
In this article, we will explore how to write npm packages using JSDoc and how it significantly enhances Developer Experience.
Example
It seems difficult to explain multiple pieces of source code in text alone, so I’ve prepared StackBlitz and Github links.
I was trying to attach StackBlitz using DEV.to’s embed, but I encountered a Failed to boot WebContainer
error.
StackBlitz
https://stackblitz.com/edit/github-p9xwsc?file=package.json
Github
https://github.com/Artxe2/jsdoc-subpkg-example
Code analysis
Starting from the package.json
file located in the project root, let’s quickly go over the important sections
// ./package.json
"scripts": {
"dts": "pnpm -r dts",
"lint": "tsc && eslint --fix .",
"test": "vitest run"
},
In the package.json
file, there are three scripts.
dts
is used for generating .d.ts
files using JSDoc, lint
performs coding convention checks, and test
is used for running tests.
// ./pnpm-workspace.yaml
packages:
- 'packages/*'
The pnpm-workspace.yaml
file is a configuration file used for managing local packages.
// ./tsconfig.json
"module": "ES6",
"moduleResolution": "Node",
"noEmit": true,
In the tsconfig.json
file, the module
and moduleResolution
options are set to ES6
and Node
, respectively, for compatibility checking. Additionally, the noEmit
option is set to true
to perform type checking only when running the pnpm lint
command.
// ./.eslintrc.json
"ignorePatterns": ["**/@types/**/*.d.ts"]
Files within the @types
folder are automatically generated, so they are excluded from eslint checks.
In the syntax
and test
folders, files are created for type checking and testing purposes. The library packages are located under the packages
folder.
// ./packages/my-lib/package.json
"exports": {
".": {
"default": "./index.js",
"types": "./@types/index.d.ts"
},
"./math": {
"default": "./src/math/index.js",
"types": "./@types/src/math/index.d.ts"
},
"./string": {
"default": "./src/string/index.js",
"types": "./@types/src/string/index.d.ts"
},
"./type-test": {
"default": "./src/type-test/index.js",
"types": "./@types/src/type-test/index.d.ts"
},
"./@types": "./src/public.d.ts"
},
"typesVersions": {
"*": {
"*": ["@types/index.d.ts"],
"math": ["@types/src/math/index.d.ts"],
"string": ["@types/src/string/index.d.ts"],
"type-test": ["@types/src/type-test/index.d.ts"],
"@types": ["src/public.d.ts"]
}
},
To define subpath modules in the library, we need several options in the package.json
file.
If the user set moduleResolution
to Node16
or NodeNext
in tsconfig.json
, the exports
option alone is sufficient.
However, for users who don’t have this configuration, we also need to set the typesVersions
option.
// ./packages/my-lib/tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"checkJs": true,
"declaration": true,
"declarationDir": "@types",
"declarationMap": true,
"emitDeclarationOnly": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "NodeNext",
"outDir": "silences wrong TS error, we don't compile, we only typecheck",
"skipLibCheck": true,
"strict": true,
"target": "ESNext"
}
}
In order to use JSDoc in a project, we need to set allowJs
and checkJs
to true
.
The outDir
option is configured in the tsconfig.json
file to suppress error messages.
If you additionally configure the declaration
, declarationDir
, declarationMap
, and emitDeclarationOnly
options, you can use the tsc command to analyze JSDoc and generate d.ts
and d.ts.map
files in the @types
folder.
Setting the module
option to NodeNext
offers several convenient benefits when using JSDoc.
// ./packages/my-lib/src/private.d.ts
/* eslint-disable no-unused-vars */
type NumberType = number;
type ConcatParam = string | number | boolean;
type A = {
type: 'A';
a(): string;
};
type B = {
type: 'B';
b(): string;
};
type C = {
type: 'C';
c(): string;
};
type ABC = A | B | C;
Typically, types are written in private.d.ts
.
To suppress ESLint extension’s error messages, we use eslint-disable no-unused-vars
.
// ./packages/my-lib/src/public.d.ts
/* eslint-disable no-undef */
export {
ConcatParam
}
To export types written in private.d.ts
, we need to write export
statements in separate file public.d.ts
.
Unfortunately, auto-completion is not supported, so we need to be careful with typos.
Similarly, to ignore error messages from VSCode extensions, we use eslint-disable no-undef
.
JSDoc
TypeScript provides static type checking to help developers identify potential errors in their code ahead of time. However, you can introduce JSDoc into an existing JavaScript project without starting from scratch, reaping the benefits. By using JSDoc to specify type information for variables, functions, classes, and more, TypeScript can also utilize this information for type checking.
// js source
/** @param {ABC} abc */
export default function(abc) {
if (abc.type == "A") return abc.a()
if (abc.type == "B") return abc.b()
return abc.c()
}
You can apply types using tags such as @type
, @param
, @return
, and similar, features like type guards are also supported without any issues.
Moreover, setting the module
option in tsconfig.json
to NodeNext
enables you to use types written in d.ts
files that do not include export
statements without any issues.
// js source
/**
* @param {import("../../public.js").ConcatParam[]} strs
*/
export default function concat(...strs) {
let result = ""
for (const str of strs) {
result += str
}
return result
}
// auto-generated d.ts
/**
* @param {import("../../public.js").ConcatParam[]} strs
*/
export default function concat(...strs: import("../../public.js").ConcatParam[]): string;
//# sourceMappingURL=concat.d.ts.map
JSDoc’s import
statements allow you to import types from other files, but they are not compatible with d.ts
files generated by the tsc
command, so it’s advisable not to use them.
/** @typedef {string | number} ConcatParam */
/**
* @param {ConcatParam[]} strs
*/
export default function concat(...strs) {
let result = ""
for (const str of strs) {
result += str
}
return result
}
// auto-generated d.ts
/** @typedef {string | number} ConcatParam */
/**
* @param {ConcatParam[]} strs
*/
export default function concat(...strs: ConcatParam[]): string;
export type ConcatParam = string | number;
//# sourceMappingURL=concat.d.ts.map
@typedef
tag is also not recommended for use due to similar compatibility issues.
Conclusion
We have covered in detail how to create an npm package using JSDoc, including the subpath module.
To wrap things up, I’d like to share a YouTube video titled “CREATOR OF SVELTE From TS TO JSDoc??” with you.
Thank you.