Turn JavaScript Into TypeScript Compatible Packages | by Jason Sturges | Feb, 2022

Generate TypeScript suitable NPM packages from pure JavaScript tasks

Maybe as a JavaScript developer, you’ve been apprehensive to transform your NPM packages to TypeScript. Or, possibly you’ve gotten legacy codebases not value investing time to port over.

Nevertheless, you’d like your JavaScript packages inclusive to the TypeScript neighborhood, capable of leverage your NPM bundle of their tasks.

In my JavaScript tasks, I’ve all the time leveraged the ability of JSDoc annotations for sorts in my tasks, which my IDE understands.

Within the JavaScript instance above, JSDoc defines the parameter and return sorts:

/**
* Return the typical of an array of numbers.
*
* @param
quantity[] arr Array of numbers to be averaged
* @returns
quantity Common of numbers
*/
const common = (arr) => arr.cut back((a, b) => a + b) / arr.size;

This empowers my IDE to offer code completion / IntelliSense that signifies the signature (arr: quantity[]) and return worth (quantity) sorts.

If I enter invalid values, it highlights errors. For instance, if I enter string values as a substitute of quantity in my common() perform:

And, I can view a full compilation of errors from my Issues window.

Even for extra complicated typedefs and enums, JSDoc supplies a number of capabilities. For instance, I’ve an enumeration of Lunar Phases that are string values:

/**
* Enumeration of lunar phases
*
*
@typedef string LunarPhase
*
@enum LunarPhase
*/
export const LunarPhase =
NEW: "New",
WAXING_CRESCENT: "Waxing Crescent",
FIRST_QUARTER: "First Quarter",
WAXING_GIBBOUS: "Waxing Gibbous",
FULL: "Full",
WANING_GIBBOUS: "Waning Gibbous",
LAST_QUARTER: "Final Quarter",
WANING_CRESCENT: "Waning Crescent",
;

Say I’ve a perform that returns the date of the following part:

/**
*
@param LunarPhase part
*
@returns Date
*/
const subsequent = (part) =>
// ...
return new Date();
;

My IDE is ready to parse these sorts:

And once more, warn of errors:

After all, JSDoc doesn’t present all of the capabilities of TypeScript reminiscent of interfaces or generic template programming. Nevertheless, it does fulfill sorts.

So now that you simply’ve written your superior JavaScript module and printed it to NPM, you’d prefer it to be leveraged inside TypeScript tasks.

After including the module to the bundle.json, you get error:

TS7016: Couldn’t discover a declaration file for module ‘<package-name>’.

Since your venture isn’t TypeScript, it didn’t constructed a d.ts declaration file, so your whole sorts are implicitly any.

There are other ways of dealing with this, from publishing @sorts packages to modifying TypeScript strictness, however in the event you’ve leveraged JSDoc as I’ve described above, there’s a technique to generate what you want.

To construct the declarations file, we’ll want so as to add some packages to our growth dependencies, configurations, and a few scripts.

Dependencies

In your bundle.json, add the next dev dependencies:

  • typescript
  • @microsoft/api-extractor

When you’re constructing from Rollup, your dev dependencies may look one thing like:

"devDependencies": 
"@microsoft/api-extractor": "^7.19.4",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.3",
"eslint": "^8.9.0",
"jsdoc": "^3.6.10",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
"rollup": "^2.67.2",
"typedoc": "^0.22.11",
"typescript": "^4.5.5"

TypeScript Configuration

For TypeScript help, we’ll want so as to add a tsconfig.json file. This can configure TypeScript to scan our JavaScript supply to construct declarations:

Add tsconfig.json to the basis of the venture:


"embody": ["./src/**/*"],
"compilerOptions":
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"outDir": "dist",
"strict": true,
"goal": "es6"

Microsoft API Extractor Configuration

To compile the declaration file, Microsoft API Extractor must be configured to merge all d.ts information right into a single index.d.ts for our bundle distribution.

Add api-extractor.json to the basis of the venture:


"projectFolder": ".",
"mainEntryPointFilePath": "<projectFolder>/construct/index.d.ts",
"bundledPackages": [],
"compiler":
"tsconfigFilePath": "<projectFolder>/tsconfig.json",
"overrideTsconfig":
"compilerOptions":
"outDir": "construct"


,
"dtsRollup":
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/index.d.ts"
,
"apiReport":
"enabled": false
,
"docModel":
"enabled": false
,
"tsdocMetadata":
"enabled": false
,
"messages":
"compilerMessageReporting":
"default":
"logLevel": "none"

,
"extractorMessageReporting":
"default":
"logLevel": "none"

,
"tsdocMessageReporting":
"default":
"logLevel": "none"



Package deal Scripts

With dependencies and configurations out of the way in which, we simply want so as to add a script to the bundle.json to run the TypeScript compiler and construct the declaration file utilizing Microsoft API Extractor.

Add the next scripts:

"scripts": 
"prebuild:sorts": "rimraf ./construct",
"construct:sorts": "tsc -p ./tsconfig.json --outDir construct && api-extractor run",
,

Earlier than constructing, it would clear the construct/ folder. That is non-compulsory, however a good suggestion. You’ll want rimraf or equal bundle for deletion.

Then, the construct:sorts script will execute TypeScript and construct declarations through Microsoft API Extractor.

When you obtain an error throughout this course of, it might be that Microsoft API Extractor requires a minimum of one TypeScript file.

You may see:

api-extractor 7.19.4 — https://api-extractor.com/

Utilizing configuration from ./api-extractor.json

ERROR: Error parsing tsconfig.json content material: No inputs had been present in config file ‘tsconfig.json’. Specified ‘embody’ paths had been ‘[“**/*”]’ and ‘exclude’ paths had been ‘[“build”]’.

Actually obnoxious… nonetheless working to establish a greater resolution right here, however mainly we simply want one TypeScript file within the root of the answer.

I create a build-declaractions.ts file with the next contents:

/**
* This file is required for Microsoft API Extractor
* to construct declaration information.
*
* It's deliberately left clean.
*/

Along with constructing the bundle, we are able to now construct TypeScript declarations by executing your construct script and construct:sorts scripts:

Your JavaScript construct distributable would be the identical as earlier than:

However now with a d.ts declarations file:

Now, your superior NPM bundle may be loaded into TypeScript tasks, as seen beneath on this React TypeScript App.tsx:

One other aspect impact of that is that we are able to leverage TypeDoc for documentation.

When you add the typedoc npm module to your dev dependencies, you’ll be able to run both of the next scripts:

"scripts": 
"predoc:jsdoc": "rimraf ./docs",
"docs:jsdoc": "jsdoc -r src/*s -R README.md -d ./docs",

"predoc:typedoc": "rimraf ./docs",
"docs:typedoc": "typedoc src --out docs",
,

We will generate JSDoc, identical as earlier than by working docs:jsdoc

Or, TypeDoc as a substitute by working docs:typedoc

An instance of this text may be discovered at GitHub from my NPM Package deal Boilerplate repository:

TypeScript is fairly superior, but it surely’s not for everybody.

For NPM Packages, TypeScript is fairly slick in that its output may be leveraged in each JavaScript and TypeScript tasks.

When utilizing JavaScript, there’s no sort declarations, which means your NPM bundle isn’t as related to the TypeScript ecosystem.

However in fact there’s an enormous ecosystem of pure JavaScript packages at NPM. TypeScript builders should deal with these conditions. That apart, in the event you leverage JSDoc and relied on it for its sort worth, this hybrid resolution simply may fulfill each worlds.

More Posts