Skip to content

Commit 7de9cd2

Browse files
Merge pull request #1286 from CleverCloud/docs/adr-typechecking
Docs: Add ADR about Type Checking
2 parents aa50ad1 + d3eb1da commit 7de9cd2

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
---
2+
kind: '📌 Architecture Decision Records'
3+
---
4+
# ADR 0027: Enabling Type Checking
5+
6+
🗓️ 2025-01-10 · ✍️ Florian Sanders
7+
8+
## Pure JavaScript, no build steps required
9+
10+
The Clever Components project is a pure JavaScript project.
11+
12+
From the start, the project was designed with the following in mind:
13+
14+
- Components should be able to run within the browser as is, without any build step,
15+
- The build step is only here to optimize the code of the components and allow bare imports,
16+
- We could use [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to support bare imports but since we want to optimize our code, we might as well rely on the bundler to handle this.
17+
18+
## JSDoc to document component APIs
19+
20+
Documentation for developers has also always been at the core of the project: if we want developers to be able to use our components in any context (with or without a JavaScript framework) we need to document APIs for every component we produce.
21+
22+
We mostly rely on `JSDoc` to document how to use our components.
23+
24+
Until now, we primarily documented public APIs of our components.
25+
Recently, we have started documenting private methods and properties to improve the experience of contributors.
26+
This makes the components easier to maintain.
27+
28+
The `JSDoc` comments of public APIs of our components (the class and its properties mainly) are processed by the `@custom-elements-manifest/analyzer`.
29+
Component docs are part of the Custom Element Manifest.
30+
The Custom Element Manifest content is formatted and displayed by Storybook.
31+
32+
## A tiny bit of TypeScript with hand-authored lib files
33+
34+
`JSDoc` is a great start but when it comes to documenting objects and more complex data structures, you may need some help.
35+
36+
And what better than TypeScript to help you document complex types in JavaScript?
37+
38+
For instance, take the following `JSDoc`:
39+
40+
```js
41+
/**
42+
* @typedef ComponentStateLoaded
43+
* @prop {'loaded'} type
44+
* @prop {string[]} list
45+
*/
46+
```
47+
48+
Even for a very simple case like this, the type structure is not obvious.
49+
Within a `d.ts` file, you could go for an interface instead:
50+
51+
```ts
52+
interface ComponentStateLoaded {
53+
type: 'loaded';
54+
list: string[];
55+
}
56+
```
57+
58+
Then, within the `.js` file, you only have to import it:
59+
```js
60+
/**
61+
* @typedef {import('../my-type-file.types.js').ComponentStateLoaded} ComponentStateLoaded
62+
*/
63+
```
64+
65+
The `d.ts` technique has several advantages:
66+
67+
- The TypeScript syntax is a lot closer to JavaScript than `JSDoc` so it's very easy to understand what you're manipulating with an `interface` or a `type`.
68+
- It is way easier to compose types and rely on [Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html) or [Generics](https://www.typescriptlang.org/docs/handbook/2/generics.html#handbook-content) in a `d.ts` file than in `JSDoc` comments.
69+
70+
We hand-author `d.ts` files for most of our components.
71+
These files usually contain types corresponding to properties and data other than JavaScript primitives and simple arrays.
72+
73+
## Why Type Checking in the first place?
74+
75+
Even if we already had `JSDoc` comments and `d.ts` files, our codebase was not actually being type checked in our editors or in CI.
76+
77+
At the beginning of the project, public docs were enough, especially for UI components because their APIs were very HTML friendly.
78+
We mostly handled primitive types, simple arrays and objects and the team was only made of 1 member so basic conventions and linting were enough to avoid most mistakes.
79+
The project grew with some fairly complicated components (`cc-env-var-*`, `cc-pricing-*`) and with it the team also grew from 1 member to 2, then 5, finally reaching 7 developers as of this writing.
80+
81+
As the team grew, we gained more resources to work on Type Checking and also more incentives to do so.
82+
83+
With pure JS and public docs, if you are not the one who produced the code or if you don't remember, you have to read the docs to understand the API, and then you have to remember it while coding.
84+
As the codebase grows, the number of things you have to keep in mind grows and the chances you make a mistake increase accordingly.
85+
86+
With Type Checking:
87+
- You can replace long comments with good naming and good types,
88+
- You get autocomplete as you code,
89+
- You get errors if you make mistakes,
90+
- You get guidance on how to correct your mistakes (although TypeScript errors are often cryptic when you're not used to it),
91+
- You get improved code navigation with more reliable reference finding and function jumping,
92+
- You get more robust refactoring capabilities since the compiler understands code relationships. For instance, renaming a component property also updates the relevant parts within story files.
93+
94+
## Why JSDoc and not TypeScript?
95+
96+
First off, using JSDoc doesn't mean we don't use TypeScript.
97+
We only use the parts of TypeScript we need.
98+
99+
The TypeScript project is composed of many things:
100+
- A bundler (to some extent),
101+
- A transpiler,
102+
- A syntax,
103+
- A type checker.
104+
105+
We prefer having as little adherence to our dependencies as possible and relying on all the features of TypeScript would mean tying our codebase to this tool.
106+
107+
We already have a bundler.
108+
We want our code to run as is within browsers so we don't want any transpiling.
109+
110+
This is why we only use TypeScript where we think it shines: as a Type Checker and as a syntax when JSDoc is not enough.
111+
112+
For us, this has huge benefits:
113+
- The code we write today is almost guaranteed to work in the future without any build step,
114+
- Breaking changes in TypeScript only impact our comments and docs, they can never break our codebase,
115+
- The config for our project feels simpler to manage and maintain.
116+
117+
Just like bundlers, linters, or even automated tests, TypeScript is a developer tool.
118+
It improves the Developer Experience and the code quality but it has very little to do with what we ship to browsers and users.
119+
It doesn't produce any actual code used at runtime.
120+
121+
When coding, the code in our editors is exactly the same as the code run within our browser, debugging is a breeze.
122+
123+
## Things we don't quite like with the JSDoc syntax
124+
125+
JSDoc's main flaw is that it's fairly verbose.
126+
Although this is a compromise we're willing to accept, there are a few cases where we'd love to see the syntax improve:
127+
- Importing and using generics with JSDoc is tedious and inconvenient, as pointed out in the following issues:
128+
- [JSDoc doesn't support generics correctly - issue #56102 - TypeScript repository](https://github.com/microsoft/TypeScript/issues/56102)
129+
- [Allow to explicitly pass type parameters via JSDoc - issue #27387 - TypeScript repository](https://github.com/microsoft/TypeScript/issues/27387)
130+
- Importing types with `@typedef` is way too verbose since you can only import one type for each `@typedef`. This has been fixed with the addition of the new `@import` and we're very eager to migrate to it as soon as we can.
131+
132+
## Managing the TypeScript version
133+
134+
We are not completely free to choose the version of TypeScript we want to use because we rely on [`CEM Analyzer`](https://github.com/open-wc/custom-elements-manifest/tree/master/packages/analyzer) to generate our docs.
135+
The [`CEM Analyzer`](https://github.com/open-wc/custom-elements-manifest/tree/master/packages/analyzer) project bundles its own version of TypeScript.
136+
Since we want to make sure the types and syntax we use are compatible with the tool we use to generate our docs, we have to use the same TypeScript version as the one bundled by [`CEM Analyzer`](https://github.com/open-wc/custom-elements-manifest/tree/master/packages/analyzer).
137+
138+
We also rely on [`lit-analyzer`](https://github.com/runem/lit-analyzer) and [`ts-lit-plugin`](https://github.com/runem/lit-analyzer/tree/master/packages/ts-lit-plugin) to both lint our code and enhance the developer experience when it comes to Lit Web Components.
139+
These tools are able to catch missing imports, unclosed HTML tags, etc. but they are not really maintained and they tend to break with the latest versions of TypeScript.
140+
141+
## On our way to a fully Type Checked codebase
142+
143+
The strategy was to progressively work on Type Checking when reworking components and add them to the CI type checking one by one.
144+
145+
To do so, here is what we did:
146+
- Reworked our `tsconfig.json` to enable Type Checking in our editors (`"allowJs": true`, `"checkJs": true`),
147+
- Decided how strict we wanted to be by enabling/disabling specific options,
148+
- Added a `tsconfig.ci.json` to enable Type Checking in CI context,
149+
- Only files that fully pass type checking, including their dependencies, can be added to the list of files checked in CI,
150+
- When we started, only one file could be added to this file.
151+
152+
## Where are we at?
153+
154+
As of this writing, we have managed to fully Type Check most of our components (UI and smart) as well as their related libs.
155+
156+
| Category | Checked | Total | Percentage |
157+
|------------------------|---------|--------|------------|
158+
| Components | 101 | 105 | 96.19% |
159+
| Components (smart) | 17 | 18 | 94.44% |
160+
| Components (test) | 0 | 98 | 0.00% |
161+
| Components (stories) | 0 | 99 | 0.00% |
162+
| Controllers | 2 | 2 | 100.00% |
163+
| Controllers (stories) | 0 | 2 | 0.00% |
164+
| Libs | 53 | 53 | 100.00% |
165+
| Tasks | 1 | 13 | 7.69% |
166+
167+
## What is left to do?
168+
169+
- Find a way to upgrade the TypeScript version when we want,
170+
- This should be possible if we move from [`custom-elements-manifest/analyzer`](https://github.com/open-wc/custom-elements-manifest/tree/master/packages/analyzer) & [`lit-analyzer`](https://github.com/runem/lit-analyzer) to [`@lit-labs/analyzer`](https://github.com/lit/lit/tree/main/packages/labs/analyzer) but this is not trivial as explained in [Investigate @lit-labs/analyzer usage - issue #1156 - Clever Components repository](https://github.com/CleverCloud/clever-components/issues/1156).
171+
- Migrate to the new `@import` syntax,
172+
- This is only possible once we've migrated to TypeScript > `5.5`.
173+
- Rely on types provided by the Clever Cloud APIs for async calls,
174+
- Currently the Clever Cloud APIs do not really expose types and a proper API documentation. Once they do, our `clever-client.js` project should be able to expose these types so we can rely on them in the components' project.
175+
- Expose the types in the npm package. This is not trivial and it impacts our bundling process. We have at least two options:
176+
- Stop minifying the sources we ship and add the `d.ts` files to the package,
177+
- Generate `d.ts` from our `.js` files like libraries do and add `d.ts` files to the package.
178+
179+
## The wishlist
180+
181+
- Better JSDoc syntax to handle generics:
182+
- [JSDoc doesn't support generics correctly - issue #56102 - TypeScript repository](https://github.com/microsoft/TypeScript/issues/27387)
183+
- [Allow to explicitly pass type parameters via JSDoc - issue #27387 - TypeScript repository](https://github.com/microsoft/TypeScript/issues/27387)
184+
- [TC39 - Type Annotations](https://tc39.es/proposal-type-annotations/)

0 commit comments

Comments
 (0)