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.
-
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.ts
file represents a.js
file next to it in the same folder.rootDir
andoutDir
.tsconfig.json
to provide editor features. This filename is also used bytsc
by default if not specified as a CLI option..ts
files..ts
files ..d.ts
files. Those should be generated bytsc
..d.ts
files..ts
files 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:
@import
tags for type imports.@nonnull
tags 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:
lib
lib
contains the source code.lib/index.js
lib/index.js
replacesindex.js
as 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.js
This 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.ts
JSDoc 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.json
This project defines how to compile the source code into type definitions.
Note that this sets
module
tonodenext
.node16
andnodenext
imply a different target.Additionally we could set
"types": []
to avoid loading all types from thenode_modules/@types
directory. Most projects don’t need them.complex-types.d.ts
Although it is rare, sometimes TypeScript performs undesirable transforms. In this case we need to author
.d.ts
hands by hand. Since.d.ts
files are not considered source files, they can exist outside thelib
directory and still be referenced.package.json
Since we break rule (1) in order to adhere to rule (2), we need point the
types
package export to the correct location. If we want to support thenode10
module resolution additionally, we also need the top-leveltypes
field.If the package needs
exports.ts
to define special types,package.json
needs to reference that file instead.We can omit the
tsc --build --clean
from the build script. This only existed as a workaround for breaking rule (2).tsconfig.json
The package’s root
tsconfig.json
applies 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.json
does not depend on any projects. It does not contain references.packages/mdx/tsconfig.json
depends on the types ofmdx
andreact
. it has the following references:packages/react/lib/tsconfig.json
does not depend on any projects. It does not contain references.packages/react/tsconfig.json
depends on the types ofmdx
andreact
. It has the following references:packages/rollup/lib/tsconfig.json
depends on the types ofmdx
. It has the following references:packages/rollup/tsconfig.json
depends on the types ofmdx
androllup
. It has the following references:The top-level
tsconfig
is known as a solution file. It contains no file, but only references. It has the following content: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 | undefined
tostring
is 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
@template
tags in JSDoc comments that have multiple@typedef
/@callback
tags. Using one@typedef
/@callback
per 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..ts
files. 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