ESM typings mismatch (Deno)
The docs say we should use window.htmx = require('htmx.org'). But that's CJS. What should I use for ESM?
I'm using esbuild with the npm htmx package.
I tried import htmx from "htmx.org". Guess the type of htmx: it's a module. But the actual runtime value is the library itself.
I should be able to write htmx.onLoad, but my LSP suggests htmx.default.onLoad instead.
For now, I'm doing this:
import htmx from "htmx.org";
import type HTMX from "htmx.org";
declare namespace globalThis {
let htmx: typeof HTMX.default;
}
globalThis.htmx = htmx as unknown as typeof HTMX.default;
(htmx as unknown as typeof htmx.default).config.methodsThatUseUrlParams.length = 0
import("htmx-ext-debug");
What I expect is this:
import htmx from "htmx.org";
htmx.config.methodsThatUseUrlParams.length = 0
globalThis.htmx = htmx;
import("htmx-ext-debug");
[!NOTE]
The plugins have bad ESM support too, and they require exposinghtmxglobally to use them.
The common advice for most ESM bundling is just:
import htmx from "htmx.org";
window.htmx = htmx;
You could maybe try something like this if your compiler/LSP is refusing to infer the types properly
import htmx from "htmx.org";
import type HTMX from "htmx.org";
globalThis.htmx = (htmx as typeof HTMX.default) || htmx as typeof HTMX;
I use TypeScript (Deno). I refuse to rely on the workaround. This is a types issue, not an LSP issue — please fix the type definitions.
The issue is we already export type definitions in a format that is supported by the majority of typescript compilers and bundling solutions but deno has unique requirements because its a little different than the rest with the way it handles types it seems. These type definitions are based on JSDoc based annotations and making them deno compatible would be a lot of work.
import htmxImport from "htmx.org";
const htmx = htmxImport.default;
// @ts-ignore: Global assignment
globalThis.htmx = htmx;
This seems to work well in a quick test deno project
I also tested modifying the types files manually to see what the changes deno expects are:
"imports": {
"htmx.org": "https://raw.githubusercontent.com/MichaelWest22/htmx/esm-test/dist/htmx.esm.js"
},
It seems like it is really an LSP issue, but I still found a potential fix. The patch adds a module declaration around the ESM typings:
--- htmx.esm.d.ts 2025-10-03 09:37:01.692385600 +0200
+++ htmx.esm.new.d.ts 2025-10-03 09:44:14.729809600 +0200
@@ -1,3 +1,4 @@
+declare module "htmx.org" {
export default htmx;
export type HttpVerb = "get" | "head" | "post" | "put" | "delete" | "connect" | "options" | "trace" | "patch";
export type SwapOptions = {
@@ -146,7 +147,7 @@
encodeParameters: (xhr: XMLHttpRequest, parameters: FormData, elt: Node) => any | string | null;
getSelectors: () => string[] | null;
};
-declare namespace htmx {
+ namespace htmx {
let onLoad: (callback: (elt: Node) => void) => EventListener;
let process: (elt: Element | string) => void;
let on: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener | any | boolean, arg4?: any | boolean) => EventListener;
@@ -211,3 +212,4 @@
let _: (str: string) => any;
let version: string;
}
+}
This patch seems to fix the problem for both deno and tsc.
deno check index.ts
tsc index.ts --noEmit
I tested with deno.enable set to both false and true in .vscode/settings.json, restarting the Deno LSP and TypeScript LSP each time with 3 different configurations: package.json (tsc/deno), deno.json.
I'm not sure how this will translate to JSDoc. /** @module htmx.org */?
yeah the core issue here is that Deno uses its own much stricter typescript module system that doesn't play well with the types generated by tsc from JSDoc sources like htmx uses. wrapping it in a module like you have above has a high risk of causing breaking change for other type consumers. this is why I was quickly testing changing the namespaces to an interface in https://github.com/bigskysoftware/htmx/compare/dev...MichaelWest22:htmx:esm-test test branch which i linked to above as an alternative test deno import. interfaces should work for all users i think. But the issue is tsc just does not support interfaces from JSDoc. And the manual post conversion seems too hacky to me. It should be possible to manually modify the htmx d.ts to be deno compatible though using your method or the method in my linked esm-test and then you can just self host in your repo or publish the custom d.ts file and link to it like
// @deno-types="./htmx.esm.d.ts"
import htmx from "htmx.org";
But my other solution of
import htmxImport from "htmx.org";
const htmx = htmxImport.default;
may be easier.
Why do we even need type definitions? Having JSDoc already makes DTS obsolete. Generated DTS files don't have TSDoc in them.
Another workaround: import htmx from "./node_modules/htmx.org/dist/htmx.esm.js".
Why do we even need type definitions?
Don't answer. import htmx from "./node_modules/htmx.org/dist/htmx.esm.js" works only in Deno. TypeScript is limited and can't import any JSDoc types.
I've also found another possible fix: package.json "type": "module". HTMX exports esm.js anyway.