DEV Community

asteinarson
asteinarson

Posted on

TypeScript / Node.js: Importing Knex into an ES6 module

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

...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:
The imported object has three type aliases

While this is how it looked (very similar code) in the new project:
Here it has only two!

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'; 
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
jvmdo profile image
João Oliveira

Thank you. It's still working on 2023, although TypeScript raises an error "This expression is not callable" on instance invocation below:

import createKnex, { Knex } from 'knex'

export const knexConfig: Knex.Config = {...}

export const knex = createKnex(knexConfig) // "This expression is not callable" 
Enter fullscreen mode Exit fullscreen mode

Perhaps it's related or not, my types declaration does not work as well

import knex from 'knex'

declare module 'knex/types/tables' {
  export interface Tables {
     MyTable: { fields... }
  }
}
Enter fullscreen mode Exit fullscreen mode

Package versions:

"knex": "^2.5.1",
"typescript": "^5.1.6",
"@types/node": "^20.4.2",
Enter fullscreen mode Exit fullscreen mode

Compiler options:

"target": "ES2020",
"module": "ESNext",
"moduleResolution": "NodeNext"
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jvmdo profile image
João Oliveira

Ok, after trying to build my project using tsup I found out that changing moduleResolution to Node the error This expression is not callable is gone.

Collapse
 
diegoferna profile image
Diego Nascimento

tks man, worked for me.

Collapse
 
buddyco profile image
buddyco

Super helpful, thanks a lot!

Collapse
 
ja_ga profile image
Janik

Started to lose my mind about this 30 mins ago, you just saved me several hours of debugging. Thank you! 🙏