microsoft/TypeScript

Support for NodeJS 12.7+ package exports

rbuckton opened this issue ยท 203 comments

NodeJS 12.7 added support for a (currently experimental) feature for custom package imports and exports in package.json: https://github.com/jkrems/proposal-pkg-exports/

In short, this feature allows a package author to redirect exports in their package to alternate locations:

{
  "name": "pkg",
  /* [...] */
  "exports": {
    "./foo": "./target.js",
    "./bar/": "./dist/nested/dir/"
  }
}

This is currently only available when --experiemental-exports is passed to NodeJS, however we should continue to track the development of this feature as it progresses.

Yeah, I know - I don't think we should implement support for it till it's stabilized - things like how . works are still being discussed in the modules group.

I agree, this issue exists primarily to serve as a place for us to track the progress of this feature in NodeJS.

Given node is now at v12.11.1 and I believe v12 will enter LTS soon has this functionality stabilised enough to warrant inclusion in typescript module resolution now?

This will prove very helpful when working with yarn simlinked monorepos as the existing types field isn't enough when there are multiple files not in the package root dir.

We spoke about it in the modules wg on Wednesday - it's going to unflag with es modules as a whole (even the cjs support), so it can be a reliable fallback-allowing mechanism for pre-esm node. It... Should... Unflag during the node 12 lifetime. But that hasn't happened yet, and details are still being fleshed out~

jgoz commented

Looks like this has unflagged in 13.2.0: https://github.com/nodejs/node/blob/v13.2.0/doc/changelogs/CHANGELOG_V13.md#notable-changes

(with support for exports)

Node 14 (next LTS) is scheduled for release 2020-04-21 and I'm guessing it will support exports unflagged as node 13 does and at that point I know I will want to use it :-). Is typescript planning to support exports in the same timeframe as the node 14 release?

Yeah, we've just been waiting for it to stabilize a bit, since it's still experimental, even if it's unflagged (it emits a warning when it's used in resolution), since we don't want to need to support an old experimental version and a final version (and it does still see significant change - there's discussion about removing the main fallback right now).

Is there a way around this for the time being? Perhaps manually linking up modules via an index.d.ts?

I'm using path based modules such as @okdecm/my-package/Databases/Postgres and thus have no index to specify as my main.
The code now works using the exports option, but tooling such as Visual Studio Code still can't resolve the modules for referencing (due to current lack of TypeScript support).

It'd be ideal if there were a way to explicitly set these in the mean time.

@okdecm, try setting the baseUrl and paths. If @my/bar depends on @my/foo then you might have a tsconfig like this:

// packages/bar/tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "rootDir": "src",
    "outDir": "dist/lib",
    "composite": true,
    "paths": {
      "@my/foo": [
        "../foo/dist/lib/index"
      ],
      "@my/foo/models": [
        "../foo/dist/lib/a/b/c/d/models"
      ],
      "@my/foo/*": [
        "../foo/dist/lib/*"
      ]
    }
  },
  "references": [{ "path": "../foo/tsconfig.json" }]
}

The paths should point to the emitted file. Remember that the paths are relative to the baseUrl so it might make sense to set the baseUrl a parent folder.

One of the ideas for conditional exports was to allow things like types per exported path.

{
  "name": "pkg",
  /* [...] */
  "exports": {
    "./foo": {
      "types": "./types/foo.d.ts",
      "default": "./target.js"
    },
    "./bar/": {
      "types": "./types/bar.d.ts",
      "default": "./dist/nested/dir/"
    }
  }
}

This would be great to see in a potential TypeScript integration. :)

Any updates on this?

I'm not sure I understand what the obstacle is? I would think that TS just needs to continue looking at the types/typings field, and load it as is. It should up to the author to declare multiple module definitions within that single d.ts file. Important TS fields/paths should not be scattered throughout the package.json file โ€“ too much room for error.

I have examples of this already. I'd expect these to Just Work โ„ข๏ธ :

This would be backwards compatible too, because a definition file that doesn't contain a declare module wrapper already assumes that the definition is the default / applies to the entire package.

An interesting tidbit is that if you load use a submodule inside a JS file, eg kleur/colors, within VSCode, the submodule's types are picked up and inferred correctly. Writing the same code inside a .ts file short circuits everything to any type.

import * as colors from 'kleur/colors';

colors.r
// (js) has code completions
// (ts) *crickets*

this exports field is stable in node api now
so it's the time to ts support it
any update?

Sort-of. Basic usecases are pretty stable, but a lot of conditional-related cases are still under discussion. Anyways, we've missed the mark for inclusion in 4.0, so 4.1 would be the earliest you'd see it.

Would TypeScript itself love to have similar options so we can decide the entry modules of a TypeScript project?

// Lib/tsconfig.json
{compilerOptions: {
  exports: [
    "index.ts" // You can only import the `index.ts` from project `Lib`
  ]
}}
// App/tsconfig.json
{references: [
  "path": "<path-to-project-Lib>"
]}
// App/x.ts
import "<path-to-project-Lib>"; // OK
import "<path-to-project-Lib>/hid"; // Type checking error: no such module `<path-to-project-Lib>/hid` 
Bnaya commented

Resolve (and dependents) support is being worked on
browserify/resolve#224

Is there any in-progress work on this and/or are there roadblocks that would need solving?

With node-v14 sceduled to enter Active LTS in under three months, i'd suspect the number of people wanting to have a feature like this to rise at an increasing rate

panva commented

Is there a way around this for the time being?

I'm currently writing a module composed of submodules only, it's TypeScript compiled for browser, node cjs, node esm. Using package.json conditional exports (wildcard syntax from v14.13.0).

Here's my workaround until TS supports the "exports" resolution scheme

Consuming such module I couldn't figure out how to get intellisense working, nor how to even consume the module from typescript since the types wouldn't load when i import submoduleB from 'module/submoduleB'. "typings" or "types" are not working, they point to a file, not a folder, i can't get TS to compile my module structure into a single .d.ts file. So i enabled --moduleResolution to see what's up and just ended up abusing the typesVersions field to get what i need.

Now my module consumers can

  • import submoduleB from 'module/submoduleB' in both ESM JS and TS
  • const submoduleB = require('module/submoduleB')
  • get intellisense
  • get types
  • ๐ŸŒดshake ๐ŸŽ‰

And most importantly, I can only bundle the type definition ONCE, even though the modules are available multiple times using different module syntaxes. I don't need to include them next to each of the module flavours.

My published folder structure

module/
โ”œโ”€โ”€ dist/
โ”‚   โ”œโ”€โ”€ browser/
โ”‚   โ”‚   โ”œโ”€โ”€ submoduleA/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ index.js
โ”‚   โ”‚   โ””โ”€โ”€ submoduleB/
โ”‚   โ”‚       โ””โ”€โ”€ index.js
โ”‚   โ”œโ”€โ”€ node/
โ”‚   |    โ”œโ”€โ”€ cjs/
โ”‚   |    โ”‚   โ”œโ”€โ”€ submoduleA/
โ”‚   |    โ”‚   โ”‚   โ””โ”€โ”€ index.js
โ”‚   |    โ”‚   โ””โ”€โ”€ submoduleB/
โ”‚   |    โ”‚       โ””โ”€โ”€ index.js
โ”‚   |    โ””โ”€โ”€ esm/
โ”‚   |        โ”œโ”€โ”€ package.json ({"type": "module"})
โ”‚   |        โ”œโ”€โ”€ submoduleA/
โ”‚   |        โ”‚   โ””โ”€โ”€ index.js
โ”‚   |        โ””โ”€โ”€ submoduleB/
โ”‚   |            โ””โ”€โ”€ index.js
|   โ””โ”€โ”€ types/
|       โ”œโ”€โ”€ submoduleA/
|       โ”‚   โ””โ”€โ”€ index.d.ts
|       โ””โ”€โ”€ submoduleB/
|           โ””โ”€โ”€ index.d.ts
โ””โ”€โ”€ package.json

My package.json contents (important ones)

{
  "exports": {
    "./*": {
      "import": "./dist/node/esm/*.js",
      "browser": "./dist/browser/*.js",
      "require": "./dist/node/cjs/*.js"
    }
  },
  "typesVersions": {
    "*": { "*": ["./types/*"] }
  },
  "files": [
    "dist"
  ]
}
Example resolution: (Click to expand)
โฏ npx tsc --traceResolution --skipLibCheck --target ES2020 --module ES2020 --moduleResolution node some.ts

======== Resolving module 'josev2/jwe/compact' from '/Users/panva/repo/esm/some.ts'. ========
Explicitly specified module resolution kind: 'NodeJs'.
Loading module 'josev2/jwe/compact' from 'node_modules' folder, target file type 'TypeScript'.
Found 'package.json' at '/Users/panva/repo/esm/node_modules/josev2/package.json'.
'package.json' has a 'typesVersions' field with version-specific path mappings.
'package.json' has a 'typesVersions' entry '*' that matches compiler version '4.0.3', looking for a pattern to match module name 'jwe/compact'.
Module name 'jwe/compact', matched pattern '*'.
Trying substitution './types/*', candidate module location: './types/jwe/compact'.
File '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts' exist - use it as a name resolution result.
Resolving real path for '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts', result '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts'.
======== Module name 'josev2/jwe/compact' was successfully resolved to '/Users/panva/repo/esm/node_modules/josev2/types/jwe/compact.d.ts' with Package ID 'josev2/types/jwe/compact.d.ts@1.0.0'. ========

@weswigham Why this feature has not been added to 4.1? Is it possible to add it to 4.2?

Node v14 is going LTS in 10 days, this is the only thing currently stopping me from using the new exports so far, would love to see this implemented.

I think we'll see a spike of people wanting to use this soon, any idea when/if we can expect support for it?

Any updates on this?

This will be great! I can't wait!! Thanks for all the great work on TS so far TS team.

csvn commented

We just encountered this when using exports on a package. The types did not work for <package-name>/test, so we had to manually add paths so that Typescript could find the correct location for the type definitions.

Looking forward to this issue being solved as well ๐Ÿ™

Just a follow up to my previous comment(s) โ€“ this approach will always work:

// package.json
{
  "name": "foobar",
  "types": "index.d.ts", // root/main module types only
  "files": [
    "*.d.ts", // root types
    "sub1", // all `foobar/sub1` files
    "sub2", // all `foobar/sub2` files
    "dist" // all `foobar` files
  ],
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    },
    "./sub1": {
      "import": "./sub1/index.mjs",
      "require": "./sub1/index.js"
    },
    "./sub2": {
      "import": "./sub2/index.mjs",
      "require": "./sub2/index.js"
    },
    "./package.json": "./package.json"
  },
  // ...
}

And then the built file structure:

foobar
โ”œโ”€โ”€ dist
โ”‚   โ”œโ”€โ”€ index.js
โ”‚   โ””โ”€โ”€ index.mjs
โ”œโ”€โ”€ sub1
โ”‚   โ”œโ”€โ”€ index.js
โ”‚   โ”œโ”€โ”€ index.mjs
โ”‚   โ””โ”€โ”€ index.d.ts # `foobar/sub1` types only
โ”œโ”€โ”€ sub2
โ”‚   โ”œโ”€โ”€ index.js
โ”‚   โ”œโ”€โ”€ index.mjs
โ”‚   โ””โ”€โ”€ index.d.ts # `foobar/sub2` types only
โ”œโ”€โ”€ index.d.ts     # `foobar` (main) types only
โ””โ”€โ”€ package.json

TypeScript gives index.d.ts the same default-resolver behavior that Node.js gives index.js. This means the sub1/index.d.ts and sub2/index.d.ts is important. So when you import { foo } from 'foobar/sub1' the sub1 directory is accessed and the index.d.ts within it is autoloaded.

The name & location of the main module's definitions (/index.d.ts in tree above) does not matter, since the "types" field points to its location.

@lukeed another workaround similar to yours works even if you don't want to change your directory structure. For example, if sub1 is actually in a src/sub1 directory, and exports has "./sub1": "./src/sub1/index.js", your workaround won't work, as your workaround needs the source directory structure to conform to the directory structure exported.

The workaround is to add a package.json in ./sub1, with two fields: main, and types, that point to /src/sub1/index.js and /src/sub1/index.d.ts respectively (or wherever your decide to put your .d.ts files).

I find that this option is better as it decouples your source code directory structure from the exports path structure.

It's equally tied to directory structure -- you're just juggling/maintaining more files to make it work.

What I presented will work. If you start changing things, you "void the warranty" so to speak ๐Ÿ˜…

Hey @lukeed, thanks for the awesome solution about module exporting. Currently I'm trying to go with your second approach, but TypeScript refuses to recognize the typing files for some reason. Can you take a look to see if I'm doing anything wrong? I've gone so far to add the index.d.ts file directly in the files, but I still get a Cannot find module 'helpers/modules' or its corresponding type declarations. error

Importing from helpers/lib/modules works properly and also importing from helpers/modules works if I suppress the TypeScript compiler with // @ts-ignore. so it seems that for some reason the typings are not recognized. Thanks for the help!

screenshot

@Shiroh1ge For this, I think you need to take the approach @giltayar just shared. Unlike my example, you are nesting/aliasing directories, which means that the "helpers/modules" doesn't actually line up with the directory structure.

My approach worked because Node.js allows the helpers/modules import, due to your defined ./modules entry, but then TS goes spelunking through the filesystem looking for a ./helpers/modules((.d)?.ts|/index(.d)?.ts match. Won't find anything since everything exists elsewhere.

Again, the approach @giltayar shared would require that you create a helpers/modules/package.json file with a types field. (I think you might only need main field I'd you're not using a bundler (eg Rollup) and are using TS compiler on its own...not sure)

I think as part of this TS would need to support conditional exports

https://nodejs.org/api/packages.html#packages_conditional_exports
https://webpack.js.org/guides/package-exports/#conditions

I think there would be a new option in tsconfig like targetConditions : ("node" | "browser" | "development" | "production")[]

Hi @giltayar. I've been trying your workaround on a package that I'm developing, but I can't get it to work. My directory structure:

my-package/
   package.json (contains "main" and "exports")   
   lib/
    index.js
    index.d.ts
    app/
      index.js
      index.d.ts
      package.json (added as per your suggestion)

I can use the package in Node.js with require('my-package/app') but importing it in TS doesn't work:

main.ts:1:25 - error TS2307: Cannot find module 'my-package/app' or its corresponding type declarations.

1 import { getApps } from 'my-package/app';

Any thoughts on what I might be missing?

I've been working on a tool that standardizes the NPM package building process called Packemon (https://packemon.dev).

It helps alleviate the subpath exports problem by using Rollup and creating single build output files (and associated type files using --generateDeclaration=api) based on input entry files. Might be a solution to all the problems listed above.

@hiranya911 you put the package.json in the app directory under lib when it should be in a top level app directory.

One of the ideas for conditional exports was to allow things like types per exported path.

{
  "name": "pkg",
  /* [...] */
  "exports": {
    "./foo": {
      "types": "./types/foo.d.ts",
      "default": "./target.js"
    },
    "./bar/": {
      "types": "./types/bar.d.ts",
      "default": "./dist/nested/dir/"
    }
  }
}

This would be great to see in a potential TypeScript integration. :)

Some tools can consume typescript directly, for example esbuild. Though perhaps out of scope for this discussion, have any of those present given thought to how those files might be mapped?

I've tentatively used an esbuild condition in some of my projects.

Thanks @giltayar. That solution worked for me. For anybody else interested, here's my package layout to work around the issue in question.

my-package/
   package.json (contains "main" and "exports")
   app/
      package.json (contains "main" and "types"; points to ../lib/app directory)     
   lib/
     index.js
     index.d.ts
     app/
        index.js
        index.d.ts

Yeah, this is getting a more painful now that Webpack 5 obeys exports.

We currently can not use libraries with exports in a TypeScript project with Webpack 5 (without inconvenience of configuring TypeScript paths).

I've been fiddling with paths for a half hour, and no luck. This is too difficult for people that don't use paths.

Currently I publish my TS packages using "main" and "types" pointing to the .ts file, but I'm hoping exports will eventually be a semantically cleaner way to do this (I don't publish TS transpiled down to JS because I can't assume what target people use, and don't want to add bloat in their builds by transpiling to ES5).

@hiranya911 Your solution was a huge help for me. Paying it forward, here are a few details that weren't quite explicit.

My project has two sub-packages (foo, bar) -- equivalent to app in the original example -- which show up in the top-level package.json as:

{
    ...
    "types": "dist/index.d.ts",
    "files": [
        "dist",
        "*.d.ts",
        "foo",
        "bar"
    ],
    "exports": {
        ".": "./dist/index.js",
        "./foo": "./dist/path/to/foo.js",
        "./bar": "./dist/path/to/bar.js",
        "./package.json": "./package.json"
    },
    ...
}

Each of the these sub-packages has a foo/package.json and bar/package.json file at the top-level. These look like:

{
    "main": "../dist/path/to/foo.js",
    "types": "../dist/path/to/foo.d.ts"
}

The types mapping here is absolutely essential. I'm not certain that we really need the main mapping since it is a duplicate of the value in the top-level package.json.

A few questions that I've encountered related to this:

  1. How do we foresee exports working together with types and typesVersions?
  2. I see some overlap with exports + workspaces (now supported by both yarn and npm) and Typescript Project References. Are they different enough to coexist?

This is workaround for subpath exports using typesVersions.
https://github.com/teppeis/typescript-subpath-exports-workaround

package.json

{
  "main": "dist/index.js",
  "types": "dist-types/index.d.ts",
  "exports": {
    ".": "./dist/index.js",
    "./exported": "./dist/exported.js"
  },
  "typesVersions": {
    "*": {
      "exported": ["dist-types/exported"]
    }
  }
}

This config exports only the package root and ./exported.

// Pass
import "typescript-subpath-exports-workaround"
import "typescript-subpath-exports-workaround/exported"

// Error
import "typescript-subpath-exports-workaround/not-exported"
import "typescript-subpath-exports-workaround/dist/exported"

Did anyone manage to get it working for plain JavaScript to make VSCode support Intellisense with exports?

This is workaround for subpath exports using typesVersions.
https://github.com/teppeis/typescript-subpath-exports-workaround

This work very well with the Typescript server used by VSCode, we got full support, but when webpack is compiling it (using ts-loader) we got an import error

Module not found: Error: Can't resolve '@bidule/package/submodule' in 'src/file.ts'

@flibustier In my test project, it works with webpack (ts-loader).
If you need help, you can raise an issue in my repo.

Would also be great if conditionals could be used to provide types for different versions of typescript. It's annoying not being able to let users take advantage of new typescript powers when publishing libraries just to maintain backwards compatibility.

Edit: Already exists: https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#version-selection-with-typesversions

This is workaround for subpath exports using typesVersions.
https://github.com/teppeis/typescript-subpath-exports-workaround

This work very well with the Typescript server used by VSCode, we got full support, but when webpack is compiling it (using ts-loader) we got an import error

Module not found: Error: Can't resolve '@bidule/package/submodule' in 'src/file.ts'

I got same issue while trying to use a package with the typesVersions workaround at some Angular 11 project (which uses webpack 4 under the hood). Any ideas?

what's the progress on this? This is a critical issue I think.

what's the progress on this? This is a critical issue I think.

Looks like its expected to be part of 4.3, set to release in May.
#42762

I just tried 4.3-beta but couldn't get it working. I tried to find some unit test or pull request for it, but no luck either. Is this really implemented? If so, is there an example somewhere? @DanielRosenwasser

I think this is a priority for TypeScript, as the whole Ecosystem will gradually migrate CommonJS modules to ESM.
And I think personnally that the outputed code from TypeScript should be as close as the source code, if the target is recent like ES2020.

My guess is that this was originally planned for 4.3 but as it doesn't appear to have landed yet likely will be out in 4.4.

Yes, we don't expect this to land for 4.3. Iteration plans describe intent to work on specific items, not necessarily that we will complete them in that time (nor is there a full commitment given that sometimes other stuff comes up as well). We are actively discussing the feature, and because each part of the ES modules story in Node is so co-mingled with each other part, it needs to be thought out carefully.

I'm speculating that types and typesVersions can be unified under package exports, with something like this:

{
  "exports": {
    ".": {
      "module": "./main.js",
      "types": {
        "typesVersion:<=4.0": "./main.ts3.d.ts",
        "default": "./main.d.ts"
      }
    },
    "./utils": {
      "module": "./lib/utils.js",
      "require": "./lib/utils.cjs",
      "types": {
        "typesVersion:<=4.2": "./lib/types/utils.ts41.d.ts",
        "default": "./lib/types/utils.d.ts"
      }
    }
  }
}
isc30 commented

any updates on this?

@rayfoss I don't know if it will help you, but after reading this issue this is the workaround that worked for me:

In my library I want to export 2 things to my clients:

import { A, B, C } from 'lib';
import { I1, I2, I3 } from 'lib/icons';

in my package.json I have

"exports": {
    ".": "./dist/index.js",
    "./icons": "./dist/icons/index.js"
},

doing this makes it work on the JS side, but typescript complained on missing the types. To make sure ts was able to find the types on the codebases where I use the library I changed the tsconfig.json to have this

"compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "lib/icons": [
        "./node_modules/lib/dist/icons" # I have the types index.d.ts in that directory as well
      ]
    }
}

It's not ideal because you need to edit the client typescript configuration, but if you control both, like in my case, it's a decent workaround.

(also I am not an expert on this stuff so it might have side-effects I don't know ๐Ÿ˜“)

isc30 commented

@marcoacierno that works fine if you are the only consumer of that package. If you publish that in NPM no one will be able to use it without hacking their build reference paths.
We really need the typescript compiler to handle this automatically

@isc30 yeah this is what I meant when I said that I control both :) The library is installed via NPM and just updating the tsconfig works so if you have a package that you use in other sites (our use case is a styleguide published on NPM that is then downloaded by our projects)

Otherwise, you could just document it in your library documentation explaining how to make typing works I guess ๐Ÿคท as I said, not ideal

My workaround, that does not require tinkering with tsconfig.json by my lib's consumers:
E.g. for

"exports": {
  ".": "./dist/index.js",
  "./react": "./dist/react/index.js"
}

I just put a file called react.d.ts into the project root with the following content:

export * from './dist/react';

This can be used by the lib's consumer out-of-the-box as import { whatever } from 'yourlib/react';

Workarounds as of today: not a single one.

I have found a workaround using the package.json setting typesVersions:

{
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*.js",
  },
  "types": "./dist/index.d.ts",
  "typesVersions": {
    "*": {
      "dist/index.d.ts": [ "dist/index.d.ts" ],
      "*": [ "dist/*" ]
    }
  }
}

With this setup, typescript correctly finds both import pkg from 'pkg' and import sub from 'pkg/sub'

isc30 commented

Notice that the workaround doesn't play well with TS Language Server autocompletions (it won't hint for proper imports for example), so there is work to be done in this area

I went through the trouble of converting everything way from subpath import patterns, only to realise that doesn't work well for scripts (pwd). Turns out, only the VSCode linter and TSC was having issues with them... everything else worked as expected, as ts-node in doesn't natively path resolution, node does.

This loaded with ts-node's esm transpile-only loader for me today. I can't get it to pass VSCode and TSC linting.
#src is defined in package.json, "Yohan" is read from an imported #src/utils.js ts file.
https://codesandbox.io/s/mocha-native-esm-ts-ctt5k?file=/src/utils.ts

Summary:

#!/usr/bin/env -S node --loader ts-node/esm/transpile-only
import { pick } from '#src/utils.js' // ts
// package.json: "imports": { "#src/*": "./src/*" }

-- I encountered this issue again two days later... I forgot I had written this... jeez. This issue must die... I will find a solution.

in the meantime multiple vendors added support for package.json exports:

could you please summarise the current blockers for typescript?
would it be possible to add support behind a feature flag?

Since all of them support custom conditions, it is crucial for TypeScript also support custom conditions, in addition to types and typesVersion. That could be an option in tsconfig/tsc like moduleResulutionCustomConditions: string[].

@jantimon

could you please summarise the current blockers for typescript?

I think, this PR sets has some initial code in this direction: #44501

Please add support for this...

ES modules are the future, and the faster we move, the faster JavaScript & TypeScript can get out of this mess

Temporary workaround i've found

package.json

{
  "type": "module",
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*"
  },
  "typesVersions": {
    "*": {
      "./dist/index.d.ts": [ "./dist/index.d.ts" ],
      "*": [ "dist/*" ]
    }
  }
}

run like so:
node --experimental-specifier-resolution=node --loader ts-node/esm ./tests.test.ts

Feedback:
I don't like the fact that I need to include .js in imports (the reason I used the node experimental specifier). It feels weird to import a typescript file with the .js extension. I know TypeScript doesn't care, but I should be able to import without an extension, and have typescript automatically append it to files in the compiled output

Feedback:
I don't like the fact that I need to include .js in imports (the reason I used the node experimental specifier). It feels weird to import a typescript file with the .js extension. I know TypeScript doesn't care, but I should be able to import without an extension, and have typescript automatically append it to files in the compiled output

@AlbertMarashi That's neither here nor there; see #16577

I don't like the fact that I need to include .js in imports (the reason I used the node experimental specifier).

@AlbertMarashi If you add the .js extension after the asterisk in the export, you dont need to include it in the import:

{
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*.js"
  }
}

I tried the following setup:

{
  "type": "module",
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*"
  },
  "typesVersions": {
    "*": {
      "*": [ "dist/*", "dist/*/index.d.ts" ]
    }
  }
}

Now everything seems to work

One thing to keep in mind: If you (or the library consumers) want to be able to import/require package.json, it must be added to the exports declaration.

Does this issue also count towards the subpath imports?
eg:
A feature in node 14 is you can define imports in your package.json and alias your relative paths in your package.json. It would be great if intellisense resolves these so using them would be even better.

An example of package.json imports:

{
  "name": "",
  "version": "1",
  "description": "",
  "main": "index.js",
  "scripts": {},
  "keywords": [],
  "author": "",
  "license": "",
  "dependencies": {},
  "devDependencies": {},
  "imports": {
    "#utils/*": "./utils/*",
    "#colors": "./commanddata/colors.json"
  }
}

Then call with require('#utils/myUtil.js');
This already works, but no intellisense is available

@MaxTechnics probably you can solve this by using paths in tsconfig.json

https://stackoverflow.com/questions/43281741/how-to-use-paths-in-tsconfig-json

Note that paths is a compiler option that works mostly for intellisense, but doesn't change import paths in output files. For the latter purpose it is common to use something like Webpack Aliases, or typescript paths plugin + ttypescript, or something else

@MaxTechnics probably you can solve this by using paths in tsconfig.json

https://stackoverflow.com/questions/43281741/how-to-use-paths-in-tsconfig-json

Note that paths is a compiler option that works mostly for intellisense, but doesn't change import paths in output files. For the latter purpose it is common to use something like Webpack Aliases, or typescript paths plugin + ttypescript, or something else

This worked! Thank you very much! Now i can finally say 'death to long relative paths'

This worked! Thank you very much! Now i can finally say 'death to long relative paths'

Be aware of some limitations when using auto import: #31173 (comment)

In fact i'm considering going back to relative imports since is quite annoying (the auto import adds invalid path i have to fix manually)

I don't really use auto imports so for me it's not really a problem, my projects are also relatively small most of the time.

We stabilized package features, including package exports, over a year ago.
Subpath patterns were stabilized shortly after.
Do you at least allow PRs to implement this feature?

It's being worked on~

Is the work targeted for inclusion in a particular release?

@conartist6 the next version according to the iteration plan.

@weswigham, for clarity - are Node.js-style exports something you're trying to address as a part of the next release (within module: node12)?

For example, will it enable consuming/creating "dual CommonJS/ES module packages"?

  "exports": {
    "import": "./index.mjs",
    "require": "./index.cjs"
  },

Looking at the above references, a lot of people think so, and wanted to clear this up. Thanks for all of the great work!

Yes, it's intended to be a part of that.

Fixed by the addition of moduleResolution: node12 with #45884 which supports these~

@weswigham I couldn't find any discussion or description about supporting the types in the package.json exports, is the syntax going to be like the comment in this thread? a "types" property?

{
  "name": "pkg",
  /* [...] */
  "exports": {
    "./foo": {
      "types": "./types/foo.d.ts",
      "default": "./target.js"
    },
    "./bar/": {
      "types": "./types/bar.d.ts",
      "default": "./dist/nested/dir/"
    }
  }
}

Typescript being able to understand a package that has the "exports" map field was the main reason for this issue

They are! A types condition is looked up, as are conditions akin to types@>=4.2!

@flibustier In my test project, it works with webpack (ts-loader). If you need help, you can raise an issue in my repo.

@teppeis I created a simple React app and able to import from typescript-subpath-exports-workaround but not from typescript-subpath-exports-workaround/exported. It is unable to find the module in the latter case.

Codesandbox link

Any help/pointers would be appreciated.

with typescript@beta it looks like not working now

@JiangWeixian The beta works for me. Just make sure compilerOptions.module equals node12 and compilerOptions.moduleResolution is undefined.

@JiangWeixian The beta works for me. Just make sure compilerOptions.module equals node12 and compilerOptions.moduleResolution is undefined.

thx, it works

They are! A types condition is looked up, as are conditions akin to types@>=4.2!

@weswigham I noticed that a types path must be .d.ts and not .ts (pointing to source file). Any reason that can't be supported?

@weswigham I noticed that a types path must be .d.ts and not .ts (pointing to source file). Any reason that can't be supported?

This is very disappointing - this will require us to introduce a build step in our monorepo development environment

@aleclarson @joshuat .ts files are not the same as .d.ts. Declarations omit types that aren't part of the public/visible API, while using the source files force consumers to all install all dependencies that are required for type information, which isn't typically what you want. Declarations avoid this.

@weswigham Should the types field be used if no types condition exists in the exports field? It seems to not be happening in the beta.

types is like a TS main - you can set it, but it'll only be used in ts versions that don't respect exports. When there is exports, we have to be able to find the types via exports, too.

That makes it very difficult for me to upgrade. There are plenty of packages out there already using exports but not including exports.types (and still using the top-level types property). I kind of expected this would work.

One great example: dotenv. It's broken in TS 4.5 even though it could work. See https://github.com/motdotla/dotenv/blob/27dfd3f034ce00b1daa72effbd91dd7788aced48/package.json#L12

@JiangWeixian The beta works for me. Just make sure compilerOptions.module equals node12 and compilerOptions.moduleResolution is undefined.

Can you provide a code example? I tried almost every possible configuration option but I could not get it working.

@JiangWeixian The beta works for me. Just make sure compilerOptions.module equals node12 and compilerOptions.moduleResolution is undefined.

Can you provide a code example? I tried almost every possible configuration option but I could not get it working.

{
  "compilerOptions": {
    "allowJs": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "preserve",
    "lib": ["dom", "es2017"],
    "module": "node12",
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "es5",
    "baseUrl": ".",
    "noImplicitThis": true,
    "paths": {
      "~/*": ["./*"],
      "@/*": ["pages/*"]
    },
    "isolatedModules": true
  }
}

I use this tsconfig.json on my nextjs project, just make sure vscode use typescript@4.5. (But there is some editor lint error, maybe fixed if vscode@latest released)

What is the current status of this issue?
Is exports long been supported in TypeScript installations, or is it a new thing, or is it not supported yet?
Will it work in most TypeScript installations without any additonal configuration?

I also heard of this workaround:

"typesVersions": {
  "*": {
    "export1": ["build/export1.d.ts"],
    "export2": ["build/export2.d.ts"]
  }
}

Does it work? Will it work in most TypeScript installations without any additonal configuration?


Update:

@andrewbranch So

TypeScript requires the following flag to be set manually in tsconfig.json in order for Node ES Modules to be enabled:

{
    "compilerOptions": {
        "module": "nodenext",
    }
}

Then, when providing separate "typings" for a library, use the types sub-property of exports entries to specify the path to the "typings" file.
In such case, types must be the first sub-property.

{
  "type": "module",
  "exports": {
    ".": {
      "types": "./index.d.ts",
      "import": "./index.js",
      "require": "./index.cjs"
    }
  }
}

Also, in *.d.ts files, any relative imports have to use the full file extension.

So, change things like:

import ... from './other-file'

or:

import ... from './other-file.d'

to:

import ... from './other-file.d.ts'

I've previously tested fully-specified file paths in *.d.ts in non-ESM mode and it didn't work, so perhaps importing by a full *.d.ts path inside a *.d.ts file only works for Node ES modules and doesn't work in non-ESM mode.

The following types path is assumed implicitly by TypeScript compiler.

{
  "type": "module",
  "exports": {
    ".": {
      // "types": "./index.d.ts",
      "import": "./index.js",
      "require": "./index.cjs"
    }
  }
}

I believe in this case it will look for index.d.ts for import and index.d.cts for require.

So, change things like:

import ... from './other-file'

or:

import ... from './other-file.d'

to:

import ... from './other-file.d.ts'

No, change them to './other-file.js' (or .mjs or .cjs where appropriate).

I believe in this case it will look for index.d.ts for import and index.d.cts for require.

I see, there'd have to be separate folders for esm and cjs then, with a package.json in each โ€” one with type: module and the other without.

No, change them to ./other-file.js (or .mjs or .cjs where appropriate).

Yeah, I was referring to TypeScript "typings" exclusively, not to TypeScript code in general.
I don't use TypeScript but people usually ask for "typings" so I provide separate ones, and in those *.d.ts files importing by a full path doesn't work for some weird reason, but importing by an incomplete path like *.d or * works.