zustand: TSC throws an error when compiling with "moduleResolution": "NodeNext"

When I want to import the default create function from zustand/vanilla while setting NodeNext for both module and moduleResolution in tsconfig.json, TSC will throw an error: error TS2349: This expression is not callable.

I mentioned this issue in https://github.com/microsoft/TypeScript/issues/50058#issuecomment-1288155712. However, according to the reply, this is a zustand problem:

@zustand/vanilla has incorrect types, again, it only declares types for a cjs entrypoint for everything, and that is the result of that mismatch.

Kindly request a fix on this problem, thanks!

In the meantime, I use default-import as a temporary patch.

this doesn’t work

import create from "zustand/vanilla";
export const store = create(() => ({ foo: "bar" }));

this works

import zustand from "zustand/vanilla";
import { defaultImport } from "default-import";
const create = defaultImport(zustand);
export const store = create(() => ({ foo: "bar" }));

A minimal repo to reproduce this problem: https://stackblitz.com/edit/node-vj368k?file=src/index.ts

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 19 (9 by maintainers)

Most upvoted comments

Sounds great!

Just about package.json: Does this work?

"./vanilla": {
  "types": "./vanilla.d.ts",
  "module": "./esm/vanilla.js",
  "import": {
    "types": "./esm/vanilla.d.mts", // trying to apply this only for "import"
    "default": "./esm/vanilla.mjs",
  },
  "default": "./vanilla.js"
},

Unfortunately no, TSC will use ./vanilla.d.ts as the resolution result, but if you change the order like this, it will work:

"./vanilla": {
  "import": {
    "types": "./esm/vanilla.d.mts",
    "default": "./esm/vanilla.mjs"
  },
  "types": "./vanilla.d.ts",
  "module": "./esm/vanilla.js",
  "default": "./vanilla.js"
},

I have some promising findings:

If I change the package.json > exports > ./vanilla > types field from "./vanilla.d.ts" to "./esm/vanilla.d.mts" like this:

"./vanilla": {
  "types": "./esm/vanilla.d.mts",
  "module": "./esm/vanilla.js",
  "import": "./esm/vanilla.mjs",
  "default": "./vanilla.js"
},

and add a new file: dist/esm/vanilla.d.mts whose content is copied from dist/esm/vanilla.d.ts (We can safely copy the content because no import or export is used in vanilla, so there’s no .js extension issues, but of course in order for other exported subpaths to work, we’ll have to consider this difference). We can finally get a successful output:

success Cleared cache.
======== Resolving module 'zustand/vanilla' from '/home/secant/zustand-esm-test/src/index.ts'. ========
Loading module 'zustand/vanilla' from 'node_modules' folder, target file type 'TypeScript'.
File '/home/secant/zustand-esm-test/node_modules/zustand/esm/vanilla.d.mts' exist - use it as a name resolution result.
Resolving real path for '/home/secant/zustand-esm-test/node_modules/zustand/esm/vanilla.d.mts', result '/home/secant/zustand-patch/dist/esm/vanilla.d.mts'.
======== Module name 'zustand/vanilla' was successfully resolved to '/home/secant/zustand-patch/dist/esm/vanilla.d.mts' with Package ID 'zustand/esm/vanilla.d.mts@4.1.3'. ========
Done in 1.82s

As I understand it, the .d.mts extension is telling TSC to regard this type file as a type file of a package imported as an esmodule rather than a commonjs module, or otherwise because we don’t have type: "module" field in the package.json, TSC seems cannot decide which type of module we are expecting.

I think I find the reason why zustand does not work properly as an esm import.

While I was searching for related problems, I bumped into this solved issue: https://github.com/hayes/pothos/issues/597, which was also an esm module resolution problem. The author finally got his package fixed after several pushes and I noticed one of his comments:

thanks, I think I see what’s going in. Looks like some imports in the esm definintions are not working correctly because they are importing directories rather than explicitly asking for dir/index.js

and

… Needed to transform all the imports to include the full file path…

So I first checked his package.json:

  "main": "./lib/index.js",
  "types": "./dts/index.d.ts",
  "module": "./esm/index.js",
  "exports": {
    "import": {
      "default": "./esm/index.js"
    },
    "require": {
      "types": "./dts/index.d.ts",
      "default": "./lib/index.js"
    }
  },

And according to this configuration, typescript will use ./dts/index.d.ts when this module is resolved as a cjs module and will use ./esm/index.d.ts when it is resolved as an es module. By comparing these two typedef files, we can find that the .js extension is needed in all the export or import statements in an esm typedef file:

diff ./esm/index.d.ts ./dts/index.d.ts
@@ -1,11 +1,11 @@
-import './types/global';
-import SchemaBuilderClass from './builder';
-import type { FieldKind, NormalizeSchemeBuilderOptions, SchemaTypes } from './types';
-export * from './plugins';
-export * from './types';
-export * from './utils';
+import './types/global/index.js';
+import SchemaBuilderClass from './builder.js';
+import type { FieldKind, NormalizeSchemeBuilderOptions, SchemaTypes } from './types/index.js';
+export * from './plugins/index.js';
+export * from './types/index.js';
+export * from './utils/index.js';
 declare const SchemaBuilder: {
-    new <Types extends Partial<PothosSchemaTypes.UserSchemaTypes> = {}>(options: import("./types").RemoveNeverKeys<PothosSchemaTypes.SchemaBuilderOptions<PothosSchemaTypes.ExtendDefaultTypes<Types>>>): PothosSchemaTypes.SchemaBuilder<PothosSchemaTypes.ExtendDefaultTypes<Types>>;
+    new <Types extends Partial<PothosSchemaTypes.UserSchemaTypes> = {}>(options: import("./types/index.js").RemoveNeverKeys<PothosSchemaTypes.SchemaBuilderOptions<PothosSchemaTypes.ExtendDefaultTypes<Types>>>): PothosSchemaTypes.SchemaBuilder<PothosSchemaTypes.ExtendDefaultTypes<Types>>;
     registerPlugin: typeof SchemaBuilderClass.registerPlugin;
     allowPluginReRegistration: boolean;
 };
@@ -23,10 +23,10 @@ export declare type ObjectFieldBuilder<Types extends SchemaTypes, ParentShape> =
 export declare const ObjectFieldBuilder: new <Types extends SchemaTypes, ParentShape>(name: string, builder: SchemaBuilderClass<Types>) => PothosSchemaTypes.ObjectFieldBuilder<Types, ParentShape>;
 export declare type InterfaceFieldBuilder<Types extends SchemaTypes, ParentShape> = PothosSchemaTypes.InterfaceFieldBuilder<Types, ParentShape>;
 export declare const InterfaceFieldBuilder: new <Types extends SchemaTypes, ParentShape>(name: string, builder: SchemaBuilderClass<Types>) => PothosSchemaTypes.InterfaceFieldBuilder<Types, ParentShape>;
-export declare type InputFieldBuilder<Types extends SchemaTypes, Kind extends 'Arg' | 'InputObject' = 'Arg' | 'InputObject'> = PothosSchemaTypes.InputFieldBuilder<Types, Kind>;
+export declare type InputFieldBuilder<Types extends SchemaTypes, Kind extends "Arg" | "InputObject" = "Arg" | "InputObject"> = PothosSchemaTypes.InputFieldBuilder<Types, Kind>;
 export declare const InputFieldBuilder: new <Types extends SchemaTypes, Kind extends "InputObject" | "Arg" = "InputObject" | "Arg">(builder: SchemaBuilderClass<Types>, kind: Kind, typename: string) => PothosSchemaTypes.InputFieldBuilder<Types, Kind>;
 export declare type BaseTypeRef = PothosSchemaTypes.BaseTypeRef;
-export declare const BaseTypeRef: new (kind: 'Enum' | 'InputObject' | 'Interface' | 'Object' | 'Scalar' | 'Union', name: string) => PothosSchemaTypes.BaseTypeRef;
+export declare const BaseTypeRef: new (kind: "Enum" | "InputObject" | "Interface" | "Object" | "Scalar" | "Union", name: string) => PothosSchemaTypes.BaseTypeRef;
 export declare type EnumRef<T, P = T> = PothosSchemaTypes.EnumRef<T, P>;
 export declare const EnumRef: new <T, P = T>(name: string) => PothosSchemaTypes.EnumRef<T, P>;
 export declare type InputObjectRef<T> = PothosSchemaTypes.InputObjectRef<T>;
@@ -39,13 +39,13 @@ export declare type ScalarRef<T, U, P = T> = PothosSchemaTypes.ScalarRef<T, U, P
 export declare const ScalarRef: new <T, U, P = T>(name: string) => PothosSchemaTypes.ScalarRef<T, U, P>;
 export declare type UnionRef<T, P = T> = PothosSchemaTypes.UnionRef<T, P>;
 export declare const UnionRef: new <T, P = T>(name: string) => PothosSchemaTypes.UnionRef<T, P>;
-export { default as BuildCache } from './build-cache';
-export { default as BuiltinScalarRef } from './refs/builtin-scalar';
-export { default as FieldRef } from './refs/field';
-export { default as InputTypeRef } from './refs/input';
-export { default as InputFieldRef } from './refs/input-field';
-export { ImplementableInputObjectRef } from './refs/input-object';
-export { ImplementableInterfaceRef } from './refs/interface';
-export { ImplementableObjectRef } from './refs/object';
-export { default as OutputTypeRef } from './refs/output';
-//# sourceMappingURL=index.d.ts.map
\ No newline at end of file
+export { default as BuildCache } from './build-cache.js';
+export { default as BuiltinScalarRef } from './refs/builtin-scalar.js';
+export { default as FieldRef } from './refs/field.js';
+export { default as InputTypeRef } from './refs/input.js';
+export { default as InputFieldRef } from './refs/input-field.js';
+export { ImplementableInputObjectRef } from './refs/input-object.js';
+export { ImplementableInterfaceRef } from './refs/interface.js';
+export { ImplementableObjectRef } from './refs/object.js';
+export { default as OutputTypeRef } from './refs/output.js';
+//# sourceMappingURL=index.d.ts.map

And the reason is that typescript doesn’t mutate any import or export statements when it compiles user’s source code to generate typedefs. Therefore, if a package using typescript is not of type module while planning to support esm build and allowing other esm typescript projects to import it, the package will have to also include typedefs specifically for esm imports.

And that’s why this problem occurs in zustand because it only generated cjs typedefs, i.e. no “.js” extensions in the export or import statements:

index.d.ts, this affects zustand:

export * from './vanilla';
export * from './react';
export { default as createStore } from './vanilla';
export { default } from './react';

middleware/devtools.d.ts, this and other middleware typedefs affect zustand/vanilla:

import type { StateCreator, StoreApi, StoreMutatorIdentifier } from '../vanilla';

declare module '../vanilla' {
    interface StoreMutators<S, A> {
        'zustand/devtools': WithDevtools<S>;
    }
}

I am not familiar with the bundle/build scripts in generating or transforming these files, but I hope the above provided info can help you or other contributors locate the problem and fix it. ❤️