A while back I wrote a file based import in TypeScript as a Node.js cli app. I used Knex for it, in a rather simple way, starting out from this code:
import Knex from 'knex'
import { Dict } from './utils.js'
let _knex: Knex;
export function connect(connection: Dict<string>) {
let conn = {
client: 'pg',
connection
}
_knex = Knex(conn as Knex.Config);
}
It just worked, and I didn't think much into why it did, at the time.
I should add here that for Node.js projects I've tried to move over to using ES6 modules in my server side code (away from CommonJS). That can cause challenges, at times.
Yesterday, I wanted to do something similar, so I started a new project. A new package.json, and new tsconfig.json and I copied and pasted the code below. It no longer worked!
After a while, I found out that knex.js was resolved to version 0.21.18 in my original project and to 0.95.4 in my new project (by means of package.json).
Reconfiguring my new project back to CommonJS, I got it working, via this line in tsconfig.json:
"module": "CommonJS", // It was 'ESNext' before
...and the corresponding in package.json (I removed 'type': 'module').
But I did not want to run my code on the server in CommonJS mode!
I felt the frustration of simply copying code and settings that has worked well before and sitting there with errors in my terminal... What had changed?
Different versions of Knex.js
So there was this significant jump, from v0.21.18 to v0.95.4. The issue must be there, somewhere.
I opened my two projects next to each other and popped up IDE type hints for the same imported Knex object. This is how it looked in the old project:
While this is how it looked (very similar code) in the new project:
If you look closely, you see that the first image contains a type alias for the Knex interface - this is missing in the second picture. In both cases, the code (behind the type hints) is:
import Knex from 'knex';
In the first case, the symbol Knex is apparently both the interface type of the Knex package and the function one can invoke, to connect with the database (the default export in CommonJS).
In the second case, the type information is not there anymore in the default import - it is just a function (with a signature). (If you look at my initial code segment you see that the exact identifier Knex is used in two quite different ways).
That was the difference.
How TypeScript gets away with using the same identifier as
- A type (the Knex interface)
- A function to be called
... I don't understand. But that was what dropped away between the earlier and later version of Knex.
Solution 1
So my change then was to name another import (to get both the function and the interface):
import { knex, Knex } from 'knex';
Then my new code actually builds and runs... but only in CommonJS mode. Built as an ES6 module, I get this on launching it:
$ node lib/cmd.js
file:///home/arst/src/mifl/lib/cmd.js:4
import { knex } from 'knex';
^^^^
SyntaxError: Named export 'knex' not found. The requested module 'knex' is a CommonJS module...
At this point... it felt like I had exhausted my ways forward. However, I remembered that the code originally was just one single default import. What about keeping that one, and on top of that, doing a named import of the TS interface?
Solution 2
This then was my new attempt:
import knex, { Knex } from 'knex';
let knex_conn: Knex;
async function connect(connection: Record<string, string>) {
let conn = {
client: 'pg',
connection
}
knex_conn = knex(conn);
return knex_conn;
}
And that turns out to work just fine, both when the code is built and run as CommonJS and as an ES module.
The key point is that the interface type and the function are two different things. And... to get to the actually exported object (from Knex.js), we have to use a default import from an ES module.
Summary
It took me a few hours to experiment my way here, and I did not really find very good TypeScript examples using this combination of both default and named imports - particularly when used from an ES module, neither in Knex documentation nor anywhere else.
What I did find (in issues for Knex on GitHub) was that people had issues running imports in TypeScript, and that some solved it by converting their code into CommonJS.
Knowing that things had worked just fine from an ES module for me, a year before, made me want to dig deeper. Thus this post.
I would guess the same pattern applies to many other primarily CommonJS based packages that one want to import with TypeScript bindings.
Top comments (5)
Thank you. It's still working on 2023, although TypeScript raises an error "This expression is not callable" on instance invocation below:
Perhaps it's related or not, my types declaration does not work as well
Package versions:
Compiler options:
Ok, after trying to build my project using
tsup
I found out that changingmoduleResolution
toNode
the errorThis expression is not callable
is gone.tks man, worked for me.
Super helpful, thanks a lot!
Started to lose my mind about this 30 mins ago, you just saved me several hours of debugging. Thank you! 🙏