58

I want to take advantage of the new-ish "exports" feature of Node.js/package.json so that I can do the following:

"exports": {
  ".": "./dist/index.js",
  "./foo": "./dist/path/to/foo.js"
}

and users can do the following:

import { foo } from 'my-package/foo';

TypeScript 4.5 should support the "exports" field, yet it does not seem to work. I am building a simple package using TS 4.5.2, and I am consuming that package in a project using TS 4.5.2. I have looked at other SO questions and this GitHub thread and this bug report but can't seem to find a consensus on the issue and whether it should work today.

I am still able to import using the more verbose syntax:

import { foo } from 'my-package/dist/path/to/foo.js';

I have also tried the object notation for exports, to no avail:

"exports": {
  ".": { "require": "./dist/index.js", "import": "./dist/index.js" },
  "./foo": { "require": "./dist/path/to/foo.js", "import": "./dist/path/to/foo.js" }
}

Is this feature ready to be used with TypeScript projects today? If yes, what am I missing? Specifics about tsconfig would be useful for both the source project and consuming project. The TS compiler complains about node12/nodenext being used for either the module or moduleResolution fields (I am definitely using TS 4.5.2).

5
  • 4
    I appreciate you keeping this question updated. They've really helped. Commented Apr 22, 2022 at 15:35
  • I think here you mean moduleResolution, not module? Commented Jul 14, 2022 at 7:33
  • 1
    @DanielleMadeley Changing module affects moduleResolution: typescriptlang.org/tsconfig#module Commented Jul 14, 2022 at 14:06
  • 3
    @RyanWheale do you by any chance have a pointer to a fully working example? I'm still struggling to make this work :( Commented Aug 28, 2022 at 21:01
  • 1
    @Stvad - I don't - but you should be able to follow any blog post or article about using exports and it should work as expected. I recommend starting small and going from there. If you're trying to retrofit an existing project, something else is probably getting in your way. This whole process if fairly simple and straightroward. The most complex part is the tsconfig. See the tl;dr section above - that should be all you need, but I'd recommend following a recent blog post for the most up-to-date instructions. Commented Aug 30, 2022 at 9:33

4 Answers 4

41

Here's the breakdown:

  • Node.js has supported exports since v12.7.0 (Jul. 2019)

    When I asked this question (Dec. 2021), Node.js had supported the exports field for nearly 2.5 years. It seemed reasonable to assume that TypeScript supported it.

  • The latest version of TypeScript at that time (4.5) did not support the exports field.

    This was particularly confusing because the TS 4.5 beta announcement said that it would supportpackage.json exports.

  • Typescript 4.7 (June 2022) finally supported package.json exports

  • Using typesVersions in package.json is not the solution

    Several people suggested using typesVersions - but that's a completely separate feature which is specific to TypeScript only (read more about it here). The exports field in package.json is a feature of node and should work with any npm module.

So, if you have a TypeScript project and you want to be able to import a package which uses package.json "exports", you will need to do the following:

  • Your TypeScript project must be using TS v4.7 or later
  • Your tsconfig should be using moduleResolution of node16 or nodenext.
    • You don't have to set moduleResolution if you are using module with a value of CommonJS, ES2015, ES6, ES2020, or ESNEXT
Sign up to request clarification or add additional context in comments.

7 Comments

The new moduleResolution of bundler supports package.json "imports" and "exports" as well, but unlike the Node.js resolution modes, bundler never requires file extensions on relative paths in imports. (typescriptlang.org/tsconfig#moduleResolution)
I personally don't mind the file extensions and the static analysis it affords - and I highly recommend that you use extensions. This can have a huge performance impact on your app startup time because of the extra file system lookups required to support extension-less imports: typescriptlang.org/docs/handbook/module-resolution.html
I don't mind your opinion either. I just supplemented your answer with a relatively new option to make a whole picture. In my case it's just default Vite.js setting
"You don't have to set moduleResultion if you are using module with a value of CommonJS, ES2015, ES6, ES2020, or ESNEXT" if module is not set to nodenext or node16, you MUST set the moduleResolution, according to the latest Typescript docs. "Classic if module is AMD, UMD, System, or ES6/ES2015; Matches if module is node16 or nodenext; Node otherwise." typescriptlang.org/tsconfig#moduleResolution
Thanks @0xCourtney - thanks for the clarification. Yes, all of the above information applies to the "consuming" typescript project. JS and Node (without typescript) can handle the exports - this whole question was about "I've got a typescript project and I need to import a package which uses package.json exports". I'll update the answer to clarify.
|
15

Without knowing what error you are getting, or in what other way TypeScript doesn't seem to be working for you (not sure why you would not want to share such crucial information), I can tell that your exports section appears to be missing types information. Typically, if your .d.ts files were located next to their respective .js files, your exports section would look like this:

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "default": "./dist/index.js"
  },
  "./foo": {
    "types": "./dist/path/to/foo.d.ts",
    "default": "./dist/path/to/foo.js"
  }
}

3 Comments

I think it's clear that I can import using the file path, but not the shortened version per my "exports". There's no error to give you, it just doesn't work. Also, the types are not necessary as TS looks for a .d.ts file next to the code files as mentioned in the document I referenced in my question: devblogs.microsoft.com/typescript/…. Regardless, I gave it a try and no dice.
It doesn't work. The package.json file doesn't recognize types.
This solved it in a pure ESM project with NodeJS, ReactJS, babel + Webpack where NodeJS runs on the compiled babel files, but webpack on src. Instead of default i've used import, see conditional-exports: nodejs.org/api/packages.html#conditional-exports TS 5.1.5; without the exports types to src, the TS2305 error was thrown "exports": { ".": { "import": "./build/index.js", "types": "./src/index.ts" } } paths in tsconfig.json isn't needed, only my IDE uses it it seems
11

TypeScript only respects export maps in package.json if you use "moduleResolution": "NodeNext"(or "Node16") instead of the widespread "moduleResolution": "Node". (I guess "moduleResolution" defaults to the same value as "module", but it's hard to find documentation of this?

Update: in TypeScript 5 there is also a "moduleResolution": "bundler" option that also respects export maps. The differences between the two are:

  • "bundler" never requires file extensions on relative paths in imports
  • "NodeNext" will match the "node" export condition in export maps, but "bundler" won't (not documented yet in the TSConfig reference...haven't managed to find the issue thread where I confirmed this with the TypeScript team)

See: https://www.typescriptlang.org/docs/handbook/esm-node.html and https://www.typescriptlang.org/tsconfig#moduleResolution

A number of TS libraries out there (including many of Microsoft's own) have errors with "moduleResolution": "NodeNext" right now because of things like relative imports without explicit file extensions.

Comments

7

I've been looking to use nested folders for a design system package we created at Pipefy, and after deep research, I found how to do it.

The package exports React components, and we use to import them like this import { Button } from '@mypackage/design-system;

Later on, we've added tokens to our design system library, like Colors, Spacing, and Fonts, but we don't want to import everything from the index, it isn't productive.

After some exhaustive research, I found how to export nested folders using TypeScript. My purpose is to use tokens like this import { Colors } from '@mypackage/design-system/tokens;

To use your TypeScript lib like this you should use the typesVersions inside the package.json file.

Here I use it like this

"typesVersions": {
    "*": {
      "index": [
        "lib/components/index.d.ts"
      ],
      "tokens": [
        "lib/tokens/index.d.ts"
      ]
    }
  },

It worked like a charm for me, it would work for you too!

4 Comments

Thanks for the comment. The typesVersions has a different purpose (and semantics) than the exports field. In particular, exports is part of node's module resolution algorithm and works with plain JS. I'm publishing a package that needs to be consumed by both JS and TS projects, and your solution would not work for javascript projects - but it's a neat trick. Hope it helps someone. At the time I wrote this question, there were articles suggesting that TS 4.5 should support exports, but it was pulled last minute due to too many use cases and other issues.
Hey @RyanWheale, looks like the exports feature has returned on version 4.7 \o/ devblogs.microsoft.com/typescript/announcing-typescript-4-7/…
Yes! I've been watching closely in anticipation. I'll get the question updated.
How do we support dule modules in typesVersions like exports? Can we add two the same type definition files for CommonJS and ESM module together? ` "typesVersions": { "*": { "utils": [ "./lib/cjs/utils/index.d.ts", "./lib/esm/utils/index.d.ts" ] } } `

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.