Rethinking project layout #238
Replies: 1 comment 2 replies
-
This is the main thing I strongly disagree with. TS (the syntax) and JSDoc (the TS syntax) consist of 2 things: a) annotations; in some future we can move to types-as-comments As you also correctly note: TS tries to generate smart JS code and smart Adjacently, I’d like to start using We could improve a lot of our performance by hand writing our type definitions. So, something like: package-name
├── lib
│ ├── ...whatever
│ └── tsconfig.json // If you want this, sure, buy why?
├── index.js // Export the JS API from `lib/`
├── index.d.ts // Export the JS API from `lib/` *and* define the `interface`s and complex type things
├── package.json
└── tsconfig.json
MDX is already working without project references? So what do project references improve?
Yeah sure.
Why not
Yep. Even without |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Since we first started using types in JSDoc, TypeScript has evolved, Node.js has evolved, and also our team’s experience using TypeScript has evolved, including my own.
Below I have compiled a list of some general rules / learnings when working with TypeScript.
.d.tsfile represents a.jsfile next to it in the same folder.rootDirandoutDir.tsconfig.jsonto provide editor features. This filename is also used bytscby default if not specified as a CLI option..tsfiles..tsfiles ..d.tsfiles. Those should be generated bytsc..d.tsfiles..tsfiles directly in Node.js. This one is not relevant for the unified ecosystem, but it is for me. :)The TypeScript 5.5 iteration plan contains 2 big new features that will make working with JSDoc a lot less limiting:
@importtags for type imports.@nonnulltags as an alternative to TypeScript non-null assertions.This leaves the following big limitation that unifiedjs projects still depend on:
Based on all of the above, I suggest to make some changes to the setup of unifiedjs projects. Especially violating rule 2 causes some issues for which we have to use workarounds, such as violating rule 8.
Project layout
First of all, I suggest the following project layout:
liblibcontains the source code.lib/index.jslib/index.jsreplacesindex.jsas the main export of the project. It re-exports all public members. Additionally, it contains all public type definitions, as TypeScript has no syntax for re-exporting types.lib/package-name.jsThis file contains the implementation JavaScript. This file could have any name really. Some places, such as the Chrome console, show the base name only. This is why I like to use a distinguishable base name. If there are no internal type definitions, we could also move everything into
lib/index.js.lib/exports.tsJSDoc has limitations. Some things can only be defined in TypeScript, such as interfaces that may be augmented. If this is the case, we can add a file
exports.ts. This file re-exports everything fromlib/index.js, plus these additional types.lib/tsconfig.jsonThis project defines how to compile the source code into type definitions.
{ "compilerOptions": { "checkJs": true, // Because this project will be referenced (see below) // This also sets the rootDir to the "lib" directory "composite": true, // We already had this. I’m not really for or against it. It diverges from what most users do. "customConditions": ["development"], // We want to emit type declarations. "declaration": true, // We want to emit type declaration maps for an enhanced editor experience. "declarationMap": true, // We don’t want to emit JavaScript "emitDeclarationOnly": true, "exactOptionalPropertyTypes": true, // Exclude dom and worker types "lib": ["es2022"], "module": "nodenext", // We want to emit the type definitions to the "types" directory. "outDir": "../types", "strict": true } }Note that this sets
moduletonodenext.node16andnodenextimply a different target.Additionally we could set
"types": []to avoid loading all types from thenode_modules/@typesdirectory. Most projects don’t need them.complex-types.d.tsAlthough it is rare, sometimes TypeScript performs undesirable transforms. In this case we need to author
.d.tshands by hand. Since.d.tsfiles are not considered source files, they can exist outside thelibdirectory and still be referenced.package.jsonSince we break rule (1) in order to adhere to rule (2), we need point the
typespackage export to the correct location. If we want to support thenode10module resolution additionally, we also need the top-leveltypesfield.{ "types": "./types/index.d.ts", "exports": { "types": "./types/index.d.ts", "default": "./lib/index.js" } }If the package needs
exports.tsto define special types,package.jsonneeds to reference that file instead.{ "types": "./types/exports.d.ts", "exports": { "types": "./types/exports.d.ts", "default": "./lib/index.js" } }We can omit the
tsc --build --cleanfrom the build script. This only existed as a workaround for breaking rule (2).tsconfig.jsonThe package’s root
tsconfig.jsonapplies to everything that’s not part of the source code. This includes tests, scripts, examples, etc.Monorepos
Project references allow setting up monorepos properly. For example, let’s use the MDX repo.
packages/mdx/lib/tsconfig.jsondoes not depend on any projects. It does not contain references.packages/mdx/tsconfig.jsondepends on the types ofmdxandreact. it has the following references:{ "references": [ {"path": "../react/lib/tsconfig.json"}, {"path": "./lib/tsconfig.json"}, ] }packages/react/lib/tsconfig.jsondoes not depend on any projects. It does not contain references.packages/react/tsconfig.jsondepends on the types ofmdxandreact. It has the following references:{ "references": [ {"path": "../mdx/lib/tsconfig.json"}, {"path": "./lib/tsconfig.json"}, ] }packages/rollup/lib/tsconfig.jsondepends on the types ofmdx. It has the following references:{ "references": [ {"path": "../mdx/lib/tsconfig.json"} ] }packages/rollup/tsconfig.jsondepends on the types ofmdxandrollup. It has the following references:{ "references": [ {"path": "../mdx/lib/tsconfig.json"}, {"path": "./lib/tsconfig.json"}, ] }The top-level
tsconfigis known as a solution file. It contains no file, but only references. It has the following content:{ "files": [], "references": [ {"path": "./mdx/packages/tsconfig.json"}, {"path": "./react/packages/tsconfig.json"}, {"path": "./rollup/packages/tsconfig.json"} ] }Now running all of MDX would be a matter of running the following command from the project root:
But each package can also be built independantly. Also none if the packages conflict with each other because of globals anymore.
Type imports
Starting with TypeScript 5.5 we can use type imports. Current we use the following syntax:
This is not a type import, but a new equivalent exported type. It leads to loss of context, such as generics and documentation. it can’t be detected as unused. It also means we need to load
some-file.d.ts, even if we don’t really use it, leading to more memory usage for TypeScript for dependants.It’s better to start using the new TypeScript syntax:
Non null assertions
Non-null assertions are safer than manual type casting. It happens relatively often that we know something is not nullish. In such a case casting
string | number | null | undefinedtostringis unsafe, because it’s easy to forget one of the union members. Also other types of casting often involve needing to add additional type importsOne typedef per comment
I ran into some issues using
@templatetags in JSDoc comments that have multiple@typedef/@callbacktags. Using one@typedef/@callbackper comment solves this. I think it’s good to always use one type definition per comment for consistency.Also sometimes I overlook where one type definition ends and another starts. The visual boundary of closing and starting a new comment helps with this.
I hope the above clarifies some of the problems in the current TypeScript setup and the ideas how to solve them. None of these things need to happen overnight. Most changes don’t even affect users.
Also not everything is set in stone. Some variations include:
index.js. This was a special file name in Node.js CJS. In ESM it’s not..tsfiles. Types in JSDoc do have some limitations.One effect of these changes I didn’t think of is that this tests the user facing type definitions. Trying to make this work in the vfile repo I accidentally discovered a bug in the TypeScript emit.
Beta Was this translation helpful? Give feedback.
All reactions