What is the best way to incorporate TypeScript into a simple JavaScript project and release it as a JavaScript library with JSDoc for TypeScript users?

Issue:

I have encountered difficulties finding an efficient solution for this. Here's a summary of what I attempted:

  • To start, ensure that allowJs and checkJs are both set to true in the tsconfig.json, then include the project files accordingly.

Type checking works well when writing files within the project (intended for publication as an NPM library): a single JS file with JSDoc comments can be imported into any other JS file seamlessly.

The challenge arises when attempting to publish the project and make the types visible in downstream dependencies. For instance, exposing types in an application that installs the published project as a package using npm install the-package.

After confirming the project functions correctly locally, attempts were made to publish it. Several methods were explored, considering all source code resides in the project's src/ folder, but each approach encountered obstacles preventing it from functioning as required (with explanations provided for each method regarding why it is unsuitable):

  1. Do not produce any output:

    • Set types to src/index.js
      • TypeScript does not recognize types from JSDoc comments in JS files, rendering this method ineffective.
  2. Generate declaration files exclusively:

    • Output declarations to a separate directory

      • Enable emitDeclarationOnly by setting it to true
      • Specify the outDir as, for example, types
      • This results in only declaration files being produced in the types folder
      • In the package.json, define types as types/index.d.ts (for instance, if src/index.js serves as the package entry point exporting everything)
      • This method functions smoothly when importing from the-package; for example, `import {whatever} from 'the-package'
      • However, unlike traditional TypeScript libraries (which output JS and declarations together), this method is flawed when trying to import sub-paths - TypeScript will fail to locate the types for these files (although VS Code will conveniently execute Go To Definition leading you to the correct place, where the JSDoc-commented types exist, yet TypeScript seems to disregard them)
    • Also emit declarations next to source files

      • Enable emitDeclarationOnly by setting it to true
      • Specify the outDir as src to have the declarations generated alongside all .js files
      • Initially, this operates without issues as any consumer can successfully import items with comprehensive type definitions
      • Upon running another TypeScript build, errors may emerge stating `error TS5055: Cannot write file '/the-package/src/something.d.ts' because it would overwrite input file`. Essentially, TypeScript now treats previously written declarations as input files
  3. Produce both declaration and .js files

    • Output files to types/
      • Although effective, this setup results in duplicate JS files existing in both types/ and src/, needlessly replicating the source
      • The source location should remain consistent before and after implementing TypeScript; no alterations are desired in either developers or consumers' source locations. The primary objective of creating JS + JSDoc is to maintain a unified source of JS files that work seamlessly, regardless of whether there are type definitions for TS users
  4. Frequently remove sibling .d.ts files before every build

    • Output .d.ts files adjacent to src/*.js files, and prior to each build, execute rm -rf src/**/*.d.ts* first.
      • Failure to delete the files will lead to TypeScript generating errors like:
        error TS5055: Cannot write file '/the-package/path/to/foo.d.ts' because it would overwrite input file.
      • This approach also causes disruptions (e.g., TypeScript server, or other build tools reliant on TS types such as ESLint with TS plugins in watch mode) inevitably break due to source files vanishing and reappearing. It creates a poor developer experience beyond just the build process itself
  5. Attempt mapping using wildcards in the exports field of the package.json

    • I experimented with:
       "exports": { "./src/*": { "types": "./types/*", "default": "./src/*" } },
    • I also tested:
       "exports": { "./src/*.js": { "types": "./types/*.d.ts", "default": "./src/*.js" } },
    • Unfortunately, no success was achieved. The consumer importing a subpath like
      import {...} from 'the-package/src/path/foo.js'
      cannot access types for that specific file
  6. Place sibling .d.ts files in src/ and attempt to exclude them as input files in tsconfig.json

    • Add this to package.json:
        "include": ["src"], "exclude": ["src/**/*.d.ts"]
    • This strategy fails; with any build subsequent to generating .d.ts files, TypeScript continues giving errors akin to those observed in method 4,
      error TS5055: Cannot write file ... because it would overwrite input file

All available options are unfeasible for various reasons outlined above.

Additional Insights:

Here is the tsconfig used:

{
  "compilerOptions": {
    "outDir": "./types", // for method 2, change this to "./src"

    "allowJs": true,
    "checkJs": true,
    "strict": false,

    "module": "ESNext",
    "target": "ESNext",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "moduleResolution": "Node",
    "resolveJsonModule": true,

    "declaration": true,
    "declarationMap": true,
    "emitDeclarationOnly": true,

    "jsx": "preserve",
    "jsxImportSource": "pota",

    "sourceMap": true,
  },
  "include": ["src"],
  "exclude": ["src/**/*.d.ts"] // try to ignore src/*.d.ts files for method 6
}

Below are pertinent snippets from package.json prior to incorporating TypeScript (with segments relating to attempted methods commented out):

{
  "type": "module",
  "//types": "./types/exports.d.ts",
  "//types": "./src/exports.d.ts",
  "main": "./src/exports.js",
  "//exports": {
    "./src/*": {
      "types": "./types/*",
      "default": "./src/*"
    }
  },
}

All source files reside in src/**/*.js. If any declarative files must be generated, they are placed in types/ (with the aim of bypassing new file creation and enabling TS to retrieve types directly from JSDoc comments upon consumer importation of the lib).

To initiate the build, simply execute tsc (or npx tsc if TypeScript isn't installed globally).

Query:

How can this be accomplished without superfluous duplication of files from src/ to another directory, minus causing TS build malfunctions (due to sibling .d.ts files in src/), without disrupting tool operations through repeated deletion of sibling .d.ts files during each rebuild, and preferably avoiding the emission of any declaration files if feasible (though emitting declarations is acceptable if necessary)?

Prerequisite: In transitioning the JS project (a package) to TypeScript (using JS+JSDoc), developers working on the package should continue modifying JS files in src/ while consumers must retain the ability to utilize files in src/ (method 3 deemed impractical).

Answer №1

I came across a helpful solution (shoutout to @-n- for mentioning typesVersions!). Incorporate typesVersions in your package.json file like this:

{
  "typesVersions": {
    "*": {
      "src/*": ["types/*"]
    }
  },
}

Simply store declaration files in the types/ directory and source code in the src/ directory.

This method works as intended! It's particularly useful for vanilla JS projects seeking to integrate TypeScript support using JSDoc comments without encountering any drawbacks mentioned in the original post.

An update the following day after further experimentation:

If you utilize the exports field with wildcards in your package.json, similar to what was tried in method 5, it will be successful when the moduleResolution option is configured to NodeNext in your tsconfig.json!

This implies that employing exports in this manner is advantageous for new projects (although keep in mind there may be specific rules around which files can be imported), while the typesVersions trick is ideal for older projects (as it does not restrict specific files from being imported).

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unable to reference a property or method in Vue.js and Vuetify due to an issue with the <v-btn-toggle> button causing an error

Just started using vuetify and exploring the <v-btn-toggle> component for the first time. I'm trying to implement a toggle feature to filter my user list based on employee, manager, or admin types... Check out a snippet of my code below: <v ...

New Requirement for Angular Service: Subclass Constructor Must Be Provided or Unable to Resolve all Parameters for ClassName (?)

During a recent project, I encountered an issue while working on several services that all extend a base Service class. The base class requires a constructor parameter of HttpClient. When setting up the subclass with autocomplete, I noticed that their con ...

What is the best way to extract data from user input and display it in a table modal?

I need to retrieve all the values from the 'input' fields and display them in a modal table using JavaScript. What is the best way to achieve this? Here is my current script: <script> $(document).ready(function() { ...

"Passing an Empty String as Input in a NodeJS Application

app.post('/register',function(req,res){ var user=req.body.username; var pass=req.body.password; var foundUser = new Boolean(false); for(var i=0;i<values.length;i++){ //if((JSON.stringify(values[i].usernames).toLowerCase==JSON. ...

Mastering the correct method for passing $NODE_DEBUG_OPTION to npm-run-all in IntelliJ IDEA

Running on my system with Ubuntu 16.04, I have IntelliJ IDEA Ultimate 2017.2, node v6.11.2, and npm v3.10.10. I am trying to debug a node.js application that has the following package.json start entry: "start:" "npm-run-all --parallel serve-static open-st ...

The error message "Property 'name' does not exist on type 'User'" is encountered

When running this code, I expected my form to display in the browser. However, I encountered an error: Error: src/app/addproducts/addproducts.component.html:18:48 - error TS2339: Property 'price' does not exist on type 'ADDPRODUCTSComponent& ...

Error: The function $(...).maxlength is not recognized - error in the maxlength plugin counter

I have been attempting to implement the JQuery maxlength() function in a <textarea>, but I keep encountering an error in the firefox console. This is the code snippet: <script type="text/JavaScript"> $(function () { // some jquery ...

Tips for creating a static PHP website with a fixed navigation bar and footer

I am looking to create a webpage with a consistent horizontal navigation bar and footer, but where the content changes dynamically based on the user's interactions with the navigation. For example: <div class="nav-bar"> /* The navigation bar r ...

What is preventing Backbone from triggering a basic route [and executing its related function]?

Presenting My Router: var MyRouter = Backbone.Router.extend({ initialize: function(){ Backbone.history.start({ pushState:true }); }, routes: { 'hello' : 'sayHello' }, sayHello: function(){ al ...

Is there a way to utilize the passValue function twice within Javascript?

Is there a way to display the value of an input field in multiple spots throughout the code? Currently, my code only passes it once. How can I rewrite it to show up again later in the code? //HTML: <input type="text" name="amount" onchange="passValue ...

After a loop, a TypeScript promise will be returned

I am facing a challenge in returning after all calls to an external service are completed. My current code processes through the for loop too quickly and returns prematurely. Using 'promise.all' is not an option here since I require values obtain ...

Tips for swapping hosts in Postman and JavaScript

Is there a simple way to allow the front-end and testing teams to easily override the host header to match {tenant}.mydomain.com while working locally? I'm looking for a solution that doesn't involve constant changes. Any ideas on how I can achie ...

Retrieve data from an AJAX call and PHP script upon successful completion

Utilizing AJAX (jQuery), my aim is to submit a form to user-ajax.php and expect the page to echo back a response like "success". In my AJAX code, I am checking the response value for success but it's not working as expected. Below is the PHP code snip ...

"Troubleshooting Tip: Why Zurb Foundation for Apps CLI is Not Working Proper

Since I am new here and do not have a rating yet, I cannot comment on the relevant question about Zurb Foundation for Apps - CLI Fails. Despite trying the suggested solution mentioned in another post, I am still encountering the same issue. Unfortunately, ...

Unlock the ability to retrieve atom values beyond RecoilRoot

In my project, I am utilizing "react-three/fiber" to contain all content within a "Canvas." Additionally, I am using Recoil's atom to store states and share them with other modules inside the "Canvas." While everything works great inside the RecoilRo ...

Transfer the required node modules to Nexus

I am looking to establish a private npm repository on our Nexus server. Would it be correct to transfer the existing node_modules folder from my local machine to Nexus for this purpose? Our Jenkins machine does not have internet access, so proxying npm is ...

Filtering controls within a table are not displayed in VueJS

I have been attempting to implement date filtering in my data table based on a demo I followed. Despite meeting the filter requirements, no results are being displayed which is perplexing. Below are the imports and data from the file containing the table: ...

Vue JS: Extracting both the unique ID and value from an array of input simultaneously

I am new to Vue and currently exploring its capabilities. I am experimenting with the Element UI for Vue's user interface. Specifically, I am working with the Input Number Component, to manage an array of data. Let's assume my data is structured ...

What steps should I follow to utilize a JavaScript dependency following an NPM installation?

After successfully installing Fuse.js using npm, I am having trouble using the dependency in my JavaScript code. The website instructions suggest adding the following code to make it work: var books = [{ 'ISBN': 'A', 'title&ap ...

The system encountered an issue with the CSS code, indicating "Invalid CSS after "...inear-gradient(": expected selector, was "{"

While attempting to compile my sass code, I encountered the following error message regarding a syntax issue: /* { "status": 1, "file": "C:/Users/faido/Desktop/Productivity/sass css/project/sass/main.sass", "line": 36, "column": 9, "mes ...