'Import a JavaScript module or library into TypeScript

Over many years I've struggled with this same issue. I cannot seem to work out how to use a JavaScript library from TypeScript, reliably.

I seem to get it working by accident and then move on and not revisit such code for years until a extrinsic change forces a breakage, like today when I updated VS 2019.

I've spent days reading about modules and requires and loaders, but I get more and more confused.

Example. I want to use DayJS in a TypeScript .ts file I am writing.

Here's the sample code.

import * as dayjs from 'dayjs'
dayjs().format()

The thing I can't understand is that this syntax has never worked for me with any library for two reasons:

  1. It omits the path to the dayjs folder, for me it should be lib/dayjs
  2. It doesn't specify a file, and my previous working reference to moment.js was pointed at the moment.js file.

It's not clear whether 'dayjs' is a folder or a file.

Two issues that compound learning are:

  1. TypeScript gives the same error whether it's found something but cannot find anything to load, or whether the path is invalid/nonexistent.

error TS2307: Cannot find module 'lib/dayjs' or its corresponding type declarations.

  1. TypeScript seems to allow the import to point to a file without a file extension, for example, this code seems to satisfy and silence the error above, because on disk there is an index.js file. This "flexibility" leads to ambiguity and confusion.

    import * as dayjs from "lib/dayjs/index";

The TypeScript documentation itself doesn't give an example of importing from a JavaScript library, only from other .ts files, which it does without the extension.

Here are the DayJS files on disk.

lib\dayjs\esm          
lib\dayjs\locale       
lib\dayjs\plugin       
lib\dayjs\CHANGELOG.md 
lib\dayjs\dayjs.min.js 
lib\dayjs\index.d.ts   
lib\dayjs\locale.json  
lib\dayjs\package.json 
lib\dayjs\README.md    

The following syntax gives no error, TypeScript compiles it and even loads intellisense/documentation.

import * as dayjs from "lib/dayjs/index.d.js";

But that file doesn't exist! So my web app doesn't work because Chrome can't get it. But the following doesn't compile, even though the file exists!!

import * as dayjs from "lib/dayjs/index.d.ts";

error TS2691: An import path cannot end with a '.d.ts' extension. Consider importing 'lib/dayjs/index' instead.

However, when I do what the error suggests, well again that's a 404 for Chrome.

Does anyone know what on Earth is going on??

What's the definitive way to use a JavaScript library in TypeScript?


Update

I have now used the baseUrl and paths settings in tsconfig.json to get an import statement compiling in TypeScript which transpiles to a path that is 200 OK when deployed/hosted.

"compilerOptions": {
    
    "target": "es5",
    "module": "es2015",
    "baseUrl": "./wwwroot/",
    "paths": {
        "lib/dayjs/dayjs.min.js": [ "lib/dayjs/index" ]
    },
...

TypeScript intellisense is telling me that my usage of DayJS is correct.

var f = dayjs().format("dddd D MMMM YYYY");
var t = dayjs(date).format("dddd D MMMM YYYY");

But in the browser, Chrome reports an error:

TimeHelpers.js:16 Uncaught TypeError: dayjs is not a function

I've no clue. I might try moment again, but that gave me some other odd error where global was undefined right at the top of its source code.

Otherwise, apparently I can declare dayjs as Any to satisfy the TS compiler and then use an old-fashioned <script> tag to load it in on the page. This is nuts.


Update

I've made enough progress, by adding the following two lines to my tsconfig.json, and changing to

"esModuleInterop": true,
"allowSyntheticDefaultImports": true,

And

import dayjs from "lib/dayjs/dayjs.min.js"

And I now get an error other people are getting which is a relief, because at least I now know to not use DayJS.

https://github.com/iamkun/dayjs/issues/313


Update

Long day today, but some progress was made.

  • I've reconfigured baseUrl and paths in tsconfig.json based on what I've reread and reinterpreted from the docs.
  • I've switched to trying luxon but have problems with that.
  • VS Code and the compiler see the Luxon typings and intellisense works.
  • I've added a small tweak to my gulpfile.js to mutate an import path so that Chrome can load it when deployed, works.
  • Tomorrow I will go back and try moment or one of the others and see if I can apply my new knowledge to get one of those packages working.

I'll post a full explainer when I've got it all working.



Solution 1:[1]

Here's what I learned. Buyer beware: it may be incorrect.

Native support for modules are relatively new to JavaScript. Over the years, in order to break a large codebase up, support for modular code was provided by libraries.

JavaScript is commonly used out-of-browser as an applications language in its own right, by way of Node.js. This means there are two large use-cases to satisfy the needs of; client-side in browser code, server-side and desktop standalone application code.

Various conventions and syntaxes exist which solve the problem, such as CommonJS (aka ServerJS), AMD, UMD and ESM/ES6 Modules.

I'm not entirely sure about this, but I think for in-browser use, you have extra work to do to use modules.

  • Add an additional compilation/transpilation/bundling tool is needed to pull all the module code files together into a single deployable code file.

  • Add a module loader library which can download the dependent module JS files.

  • For ESM, ensure user's browsers are recent-ish, then make sure the import statements point to a valid URL of the JS file, including its extension.

The Node.js runtime maybe has its own syntax and module loader, I don't know.

TypeScript is just a JavaScript compiler/transpiler, its designed for Node.js and browser apps. Its output is only part of the story and usually needs further manipulation or processing. Unlike a traditional code compiler, just because the compiler is happy and error free, it doesn't automatically mean your TS-produced JS will run.

For example, in my case, I needed a find-replace step in my Gulpfile.js to change some of my import thing from "name" module names to working URLs.

TypeScript needs a typings file to help its compiler. This file is only useful during editing, for intellisense and to check your usage of the library is perfect. Once transpiled, it's not used. Many packages now come with typings files, .d.ts files, and TS often just finds them magically.

NPM packages aren't usually built for ES6. The JS files in the package may themselves be the product of transpiled source code. The documentation may show the syntax for using ESM but may not state which JS file within the package to use, or even include a compatible file or files.

You may even be expected to further compile, transpile or bundle the JS files in a package. This is often ambient knowledge, and not explicitly stated.

In the case of Luxon, the package doesn't contain ESM compatible code, but does contain code for CommonJS and the <script> tag. ESM compatible code is a manual download.

In the case of moment.js, the problem would have been solved early on if the documentation explained which moment.js file to import.

The closest I got on my own was finding moment/src/moment.js which looked like ES6 code and had a default export, but failed in the browser because that same file imports from JS files without specifying the .js extension, which is not supported by web browsers.

Finally, after help from someone on SO, I found what looks like a large, single, bundled moment.js with a default export in the dist folder. Did I mention JS package authors use arcane names?

Unfortunately, I'd been trying many different date-time packages and messing around with my TypeScript settings so much I didn't come across this sooner - I probably didn't know what to look for until the last 8 hours.

  • Within your NPM package, locate the large "pre-bundled"(?) JS file with the ES6-looking code, often discernible by searching for export default within the file, usually at the bottom.

  • Use the following tsconfig.json

    "target": "es6", "module": "es2015", "esModuleInterop": true, "allowSyntheticDefaultImports": true, // set baseUrl and paths as needed

  • Use the following syntax to consume e.g. moment.js

    import moment from 'moment/moment' // alter module name/path as needed

  • After TSC runs, use a find-replace task to turn TS-emitted module names into deployed URLs.

  • Deploy and hard reload in your browser, with the console open.

Solution 2:[2]

I share many of the same frustrations! It's so hard to get TypeScript working nicely with JavaScript and the Microsoft documentation is so obtuse!

In your case: the path to a library is always looked for in node_modules so in that case you don't need to add the full path.

You also never need to import a .d.ts file. You can just put the .d.ts file somewhere in your working folder and VS Code will detect it.

If you have the .d.ts file for moment.js, you will get type completion in VS Code. You don't need to import moment.js when you load it with a <script> file.

About importing extensions: that depends on if you use native ES6 modules or if you use a module bundler. In the latter case, you don't need to add the file extension, since the module bundler will fix that for you.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 danronmoon
Solution 2 jonrsharpe