ajv: Typescipt and ESM yields "Ajv This expression is not constructable"

I have a TS project in ESM mode (type:module) and getting VSCode errors using certain modules such as Ajv…

Given this in my tsconfig.json:

{  
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "nodenext",
    "esModuleInterop": true
    // I had a lot more stuff but the top two lines are the ones causing issue (Node16 is same)
  }
}

package.json

{
  "name": "test",
  "type": "module",
  "dependencies": {
    "ajv": "^8.11.0"
  },

  "devDependencies": {
    "@tsconfig/node16": "^1.0.3",
    "@types/node": "^18.11.0",
    "typescript": "^4.8.3"
  }
}

and finally code:

import Ajv from "ajv"
export const getClient = ():Ajv => { 
  return  new Ajv()
}

I get the error as attached Screen Shot 2022-10-15 at 9 58 18 PM

Note that this code compiles correctly.

If I do:

import  Ajv from "ajv"
export const getClient = ():Ajv.default => { 
  return  new Ajv.default()
}

Then error goes away but it does not run.

I think this is something to do with CJS modules needing to add something for esm export but not sure exactly what.

Here’s a sandbox

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 27
  • Comments: 23 (8 by maintainers)

Commits related to this issue

Most upvoted comments

If I do:

import  Ajv from "ajv"
export const getClient = ():Ajv.default => { 
  return  new Ajv.default()
}

Then error goes away but it does not run.

This does run correctly in Node.js, as @nicojs pointed out. I don’t know of any runtime or bundler where it wouldn’t run. However, it’s probably not the API that AJV intended to give to Node.js ESM users.

In the runtime CJS code, AJV uses a clever interop pattern:

module.exports = exports = Ajv;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = Ajv;

that makes Ajv exposed both on module.exports and module.exports.default. So in reality, a Node ESM importer can do both of these:

// In Node, a default import always binds to the
// `module.exports` of a CJS module!
import Ajv from "ajv";
new Ajv();         // module.exports -> Ajv
new Ajv.default(); // module.exports.default -> Ajv

However, the types don’t reflect the existence of this interop pattern. The types just say

export default Ajv;

which implies that only this part exists at runtime:

Object.defineProperty(exports, "__esModule", { value: true });
exports.default = Ajv;

Which is why TypeScript is only going to let you access the symbol on module.exports.default:

import Ajv from "ajv";
new Ajv();
//  ^^^ The only thing you told me about the `module.exports` of "ajv"
//      is that it has a `default` property with an `Ajv` class.

new Ajv.default();
//  Yeah, now you’re constructing the class that you declared to exist.

To make the types more accurate, you should do something like this:

import AjvCore from "./core";
import { Format, FormatDefinition, AsyncFormatDefinition, KeywordDefinition, KeywordErrorDefinition, CodeKeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, Vocabulary, Schema, SchemaObject, AnySchemaObject, AsyncSchema, AnySchema, ValidateFunction, AsyncValidateFunction, SchemaValidateFunction, ErrorObject, ErrorNoParams, } from "./types";
import { Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions } from "./core";
import { SchemaCxt, SchemaObjCxt } from "./compile";
import { KeywordCxt } from "./compile/validate";
import { DefinedError } from "./vocabularies/errors";
import { JSONType } from "./compile/rules";
import { JSONSchemaType } from "./types/json-schema";
import { _, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions } from "./compile/codegen";
import { default as ValidationError } from "./runtime/validation_error";
import { default as MissingRefError } from "./compile/ref_error";
declare namespace Ajv {
  const _default: typeof Ajv;
  export { _default as default, Format, FormatDefinition, AsyncFormatDefinition, KeywordDefinition, KeywordErrorDefinition, CodeKeywordDefinition, MacroKeywordDefinition, FuncKeywordDefinition, Vocabulary, Schema, SchemaObject, AnySchemaObject, AsyncSchema, AnySchema, ValidateFunction, AsyncValidateFunction, SchemaValidateFunction, ErrorObject, ErrorNoParams, Plugin, Options, CodeOptions, InstanceOptions, Logger, ErrorsTextOptions, SchemaCxt, SchemaObjCxt, KeywordCxt, DefinedError, JSONType, JSONSchemaType, _, str, stringify, nil, Name, Code, CodeGen, CodeGenOptions, ValidationError, MissingRefError };
}
declare class Ajv extends AjvCore {
  _addVocabularies(): void;
  _addDefaultMetaSchema(): void;
  defaultMeta(): string | AnySchemaObject | undefined;
}
export = Ajv;

Notice that there’s now an export = Ajv instead of export default Ajv, but the namespace Ajv also has a default export/property with the same type.

Any updates with version v8?

Same problem here in a ESM package. Here is a temp workaround:

-import Ajv from "ajv";
+import _Ajv from "ajv";
+
+const Ajv = _Ajv as unknown as typeof _Ajv.default;

const ajv = new Ajv();

This could easily be fixed by providing named exports for Ajv, and then import those:

import { Ajv } from 'ajv';

const ajv = new Ajv();

The existing default export can be left untouched to avoid breaking any backwards compatibility. I’d be happy to make a PR for that.

This is also working for me:

import ajvModule from 'ajv';
const Ajv = ajvModule.default;
const ajv = new Ajv();

(no need for an unknown type assertion)

It looks like this is still an issue with the latest version. See https://arethetypeswrong.github.io/?p=ajv%408.12.0

Published @benasher44/ajv, if folks want to give it a try

Whooops yes it lol 🤦

Draft PR here: #2365

I’m not sure if there is a better way, but I ended up making named exports in two places to make their usages in export import easier.

My bad - I had the return type on the client creation set to typeof Ajv as per VS suggestion which was wrong.