Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More user-friendly type definition files structure for TypeScript #2114

Closed
r7kamura opened this issue Dec 27, 2023 · 1 comment · Fixed by #2117
Closed

More user-friendly type definition files structure for TypeScript #2114

r7kamura opened this issue Dec 27, 2023 · 1 comment · Fixed by #2117

Comments

@r7kamura
Copy link
Contributor

r7kamura commented Dec 27, 2023

Summary

For those who want to use @ruby/prism npm package from TypeScript, how about moving the type definition files from types/*.d.ts to src/*.d.ts, or adding exports field in package.json for more entry points?

Details

Problem

Currently, if you want to import an individual file (e.g. src/parsePrism.js) on TypeScript instead of the main entry point (src/inedx.js), you need to add a setting to tsconfig.json on your side that indicates the mapping between the JavaScript file and the type definition file as follows:

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@ruby/prism/src/*": [
        "./node_modules/@ruby/prism/src/*",
        "./node_modules/@ruby/prism/types/*"
      ]
    }
  }
}
// Now the above settings allow correct reference to the type definition of the `parsePrism` function.
import { parsePrism } from "@ruby/prism/src/parsePrism.js";

If the above settings are not configured, tsc will generate an error:

example.ts:1:28 - error TS7016: Could not find a declaration file for module '@ruby/prism/src/parsePrism.js'. '/home/r7kamura/ghq/github.com/r7kamura/myapp/node_modules/@ruby/prism/src/parsePrism.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/ruby__prism` if it exists or add a new declaration (.d.ts) file containing `declare module '@ruby/prism/src/parsePrism.js';`

1 import { parsePrism } from "@ruby/prism/src/parsePrism.js";
                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Background

After reading all this, you may be tempted to ask: "Why not import from the main entry point like import { ... } from '@ruby/prism' ?"

As background, I am trying to use this package with the VSCode extension, and WASI is not available there. So when I try to import the main entry point, I get an error at:

import { WASI } from "wasi";

So, in this environment, I need to use shim for WASI, just as we do when using it in a web browser. After doing this, I need to import the individual files to use their partial implementations. I believe this is a relatively common use case for this package.

Solution A

When tsc is trying to import src/foo.js, it will use src/foo.d.ts by default if available. Therefore, this problem can be solved by eliminating the types directory and merging it with the src directory.

"type": "tsc --allowJs -d --emitDeclarationOnly --outDir types src/index.js"

-  "type": "tsc --allowJs -d --emitDeclarationOnly --outDir types src/index.js" 
+  "type": "tsc --allowJs -d --emitDeclarationOnly --outDir src src/index.js" 

There are two major schools of directory structure regarding type definition files in a TypeScript project. There is the pattern of having a types directory, and there is the pattern of cramming them into one directory.

One advantage of having a types directory is that users who are not interested in types are less likely to be aware of the existence of type definition files. Other advantages are that the files seem to be somewhat organized, and it is a little less work at build time. On the other hand, the disadvantage is that it is not friendly to users of this package in TypeScript, as it forces additional configuration.

I personally recommend this solution because it is simple and easy.

Solution B

This problem can also be solved by adding exports field to package.json to add more entry points.

// pacakge.json
{
  "exports": {
    "./prismParser": {
      "import": {
        "default": "./src/prismParser.js",
        "types": "./types/prismParser.js"
      }
    },
    "./nodes": {
      "import": {
        "default": "./src/nodes.js",
        "types": "./types/nodes.js"
      }
    },
    "./deserialize": {
      "import": {
        "default": "./src/deserialize.js",
        "types": "./types/deserialize.js"
      }
    },
    // ...
  }
}

One advantage of this method is that the types directory can be maintained. Also, since it is generally unbecoming to import internal files directly, it is good practice to provide an official entry point for such a use case.

One drawback of this approach is that it is still complicated to introduce the exports field. It was introduced in the relatively new Node.js and is related to the ESM and TypeScript specifications, so both package providers and users are required to have some knowledge about it.

Also, when using entry points, the import method on the user side will also change a little.

-import { parsePrism } from "@ruby/prism/src/parsePrism.js";
+import { parsePrism } from "@ruby/prism/parsePrism";

In addition, it is necessary to prepare as many entry points as the number of possible use cases, so it is a bit difficult to manage them. In the exports field, wildcards such as * can be used, which may make things a little easier.

@kddnewton
Copy link
Collaborator

Thanks for the report! #2117 implements your first solution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants