|
| 1 | +--- |
| 2 | +title: Generating TypeScript Definition Files from JavaScript |
| 3 | +published: false |
| 4 | +description: - |
| 5 | +tags: javascript, typescript, jsdoc, buildless |
| 6 | +--- |
| 7 | + |
| 8 | +First of let me say that I have been putting of this blog post for quite a while. |
| 9 | +I am a little afraid that I am not going to do TypeScript justice. |
| 10 | +The reason for that is that we are going to use it heavily - but in a rather indirect way. |
| 11 | + |
| 12 | +See - we are a big fan of a [buildless]() development setup. We have [a post]() or [two]() about it :grimmacing: |
| 13 | +It is [our believe](https://open-wc.org/about/rationales.html) that it is the best way to bring developers (you) and the platform (browser) back on the same table. |
| 14 | + |
| 15 | +Knowing this makes it hard to root for TypeScript as it is a [Transpiler Language]() - in other words it requires a build step. |
| 16 | + |
| 17 | +So how come we are still fans? |
| 18 | +Let's dive into and see what Types can give you. |
| 19 | + |
| 20 | +#### We will start by writing some tests in TypeScript: |
| 21 | + |
| 22 | +```js |
| 23 | +// helpers.test.ts |
| 24 | +import { square } from '../helpers'; |
| 25 | + |
| 26 | +expect(square(2)).to.equal(4); |
| 27 | +expect(square('two')).to.equal(4); |
| 28 | +``` |
| 29 | + |
| 30 | +Our plan is to accept a number and a string and return the power of it with the default of power of 2 (e.g. square it). |
| 31 | + |
| 32 | +Let's implement it with TypeScript: |
| 33 | + |
| 34 | +```ts |
| 35 | +// helpers.ts |
| 36 | +export function square(number: number) { |
| 37 | + return number * number; |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +So yeah I know what you have been thinking - a string as an argument? |
| 42 | +While implementing we found out that it was a bad idea. |
| 43 | +And thanks to the power of types we can just go back to our code/tests and tada we immeditelly see in vscode that `square('two')` is not working. |
| 44 | + |
| 45 | + |
| 46 | + |
| 47 | +And we will of course get the same if we try to run `tsc`. |
| 48 | + |
| 49 | +```bash |
| 50 | +npm i -D typescript |
| 51 | +``` |
| 52 | + |
| 53 | +```bash |
| 54 | +$ npx tsc |
| 55 | +helpers.tests.ts:8:19 - error TS2345: Argument of type '"two"' is not assignable to parameter of type 'number'. |
| 56 | + |
| 57 | +8 expect(square('two')).to.equal(4); |
| 58 | + ~~~~~ |
| 59 | + |
| 60 | +Found 1 error. |
| 61 | +``` |
| 62 | + |
| 63 | +### Let's make the same in JavaScript |
| 64 | + |
| 65 | +For the tests only the import change to `*.js`. |
| 66 | + |
| 67 | +```js |
| 68 | +// helpers.test.js |
| 69 | +import { square } from '../helpers.js'; |
| 70 | + |
| 71 | +expect(square(2)).to.equal(4); |
| 72 | +expect(square('two')).to.equal(4); |
| 73 | +``` |
| 74 | + |
| 75 | +For the code we removed the type |
| 76 | + |
| 77 | +```js |
| 78 | +// helpers.js |
| 79 | +export function square(number) { |
| 80 | + return number * number; |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +And our if we go back to the test now we do not see that `square('two')` is wrong :(. |
| 85 | + |
| 86 | + |
| 87 | + |
| 88 | +So that is the power of types. But we can make it work for JavaScript as well :hugs: |
| 89 | + |
| 90 | +Let's add a types via JsDoc |
| 91 | + |
| 92 | +```js |
| 93 | +/** |
| 94 | + * @param {number} number |
| 95 | + */ |
| 96 | +export function square(number) { |
| 97 | + return number * number; |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +and configure TypeScript to check for JavaScript as well by adding a `tsconfig.json`. |
| 102 | + |
| 103 | +```json |
| 104 | +{ |
| 105 | + "compilerOptions": { |
| 106 | + "target": "esnext", |
| 107 | + "module": "esnext", |
| 108 | + "moduleResolution": "node", |
| 109 | + "lib": ["es2017", "dom"], |
| 110 | + "allowJs": true, |
| 111 | + "checkJs": true, |
| 112 | + "noEmit": true, |
| 113 | + "strict": false, |
| 114 | + "noImplicitThis": true, |
| 115 | + "alwaysStrict": true, |
| 116 | + "types": ["mocha"], |
| 117 | + "esModuleInterop": true |
| 118 | + }, |
| 119 | + "include": ["test", "src"] |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +Doing this allows as to get exaclty the same behavior in VSCode as with TypeScript. |
| 124 | + |
| 125 | + |
| 126 | + |
| 127 | +We even get the same behavior when running `tsc`. |
| 128 | + |
| 129 | +```bash |
| 130 | +$ npx tsc |
| 131 | +test/helpers.tests.js:8:19 - error TS2345: Argument of type '"two"' is not assignable to parameter of type 'number'. |
| 132 | + |
| 133 | +8 expect(square('two')).to.equal(4); |
| 134 | + ~~~~~ |
| 135 | + |
| 136 | +Found 1 error. |
| 137 | +``` |
| 138 | + |
| 139 | +#### Enhancing our code |
| 140 | + |
| 141 | +Let's assume we want to also add an offset (best I could come up with :see_no_evil:) |
| 142 | +e.g. |
| 143 | + |
| 144 | +```js |
| 145 | +expect(square(2, 10)).to.equal(14); |
| 146 | +expect(square(2, 'ten')).to.equal(14); |
| 147 | +``` |
| 148 | + |
| 149 | +First up `TypeScript`: |
| 150 | + |
| 151 | +``` |
| 152 | +export function square(number: number, offset = 0) { |
| 153 | + return number * number + offset; |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +I assume you are wondering why we do not have a type here? |
| 158 | +It is because a default value let's TypeScript set the type based on this default value. |
| 159 | + |
| 160 | +And now the same for `JavaScript`: |
| 161 | + |
| 162 | +```js |
| 163 | +/** |
| 164 | + * @param {number} number |
| 165 | + */ |
| 166 | +export function square(number, offset = 0) { |
| 167 | + return number * number + offset; |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +In both cases it will give |
| 172 | + |
| 173 | +```bash |
| 174 | +test/helpers.tests.js:13:22 - error TS2345: Argument of type '"ten"' is not assignable to parameter of type 'number'. |
| 175 | + |
| 176 | +13 expect(square(2, 'ten')).to.equal(14); |
| 177 | + ~~~~~ |
| 178 | +``` |
| 179 | + |
| 180 | +Also in both cases the only thing we needed to add was `offset = 0` as it contains the type information already. |
| 181 | + |
| 182 | +If you wanna know more about how to use JSDoc for types I can recommend you these blog posts. |
| 183 | + |
| 184 | +- [Type-Safe Web Components with JSDoc](https://dev.to/dakmor/type-safe-web-components-with-jsdoc-4icf) |
| 185 | +- [Type Safe JavaScript with JSDoc](https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7a2a63209b76) |
| 186 | + |
| 187 | +### Publishing a library |
| 188 | + |
| 189 | +If someone is to use your code you will need to publish it. Usually that happens on npm. |
| 190 | +You will also want to provide those types to your users. |
| 191 | +That means you will need to have `*.d.ts` files in the package you are publishing. |
| 192 | +As those are the only files that `TypeScript` respects by default in the `node_modules` folder. |
| 193 | + |
| 194 | +##### What does it means for TypeScript? |
| 195 | + |
| 196 | +When we publish we will run `tsc` with these settings |
| 197 | + |
| 198 | +```json |
| 199 | +"noEmit": false, |
| 200 | +"declaration": true, |
| 201 | +``` |
| 202 | + |
| 203 | +that way TypeScript will generate `*.js` and `*.d.ts` files. |
| 204 | +It can do so fully automatic as it knows all the types - as it is TypeScript. |
| 205 | + |
| 206 | +The output will be |
| 207 | + |
| 208 | +```js |
| 209 | +// helpers.d.ts |
| 210 | +export declare function square(number: number, offset?: number): number; |
| 211 | + |
| 212 | +// helpers.js |
| 213 | +export function square(number, offset = 0) { |
| 214 | + return number * number + offset; |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +e.g. the output of the js file is exactly the same we wrote in our js version. |
| 219 | + |
| 220 | +#### What does it means for JavaScript? |
| 221 | + |
| 222 | +Sadly as of now `tsc` does not support generating `*.d.ts` files from JSDoc annotated files. |
| 223 | +But it probably will be in the future. The original [issue](https://github.com/microsoft/TypeScript/issues/7546) is from 2016 but recently it has been said was planned for `3.6` (but it didn't make it into beta) so it seems it on the board for for `3.7`. However don't take my word for it as here is a working [Pull Request](https://github.com/microsoft/TypeScript/pull/32372). |
| 224 | + |
| 225 | +And it is working so great that we are using it even in production for [open-wc](https://github.com/open-wc/open-wc/blob/master/package.json#L7). |
| 226 | + |
| 227 | +> WARNING |
| 228 | +> This is an unsupported version => if something does not work no one is going to fix it. |
| 229 | +> Therefore if your usecase is not supported you will need to wait for the offical release of TypeScript to support it. |
| 230 | +
|
| 231 | +So you have been warned if you still think it's a good idea to test it you feel free to do so. |
| 232 | +We published a forked version [typescript-temporary-fork-for-jsdoc](https://www.npmjs.com/package/typescript-temporary-fork-for-jsdoc) which is just a copy of what the above Pull Request is providing. (again to be clear - we did not change anything it is a temporary fork which is good enough for our use case). |
| 233 | + |
| 234 | +## Generate TypeScript Definition Files for JSDoc annotated JavaScript |
| 235 | + |
| 236 | +So now that we have all the information. Let's just make it work. |
| 237 | + |
| 238 | +1. Write your code in js and apply JSDoc where needed |
| 239 | +2. Use the forked TypeScript `npm i -D typescript-temporary-fork-for-jsdoc` |
| 240 | +3. Have a `tsconfig.json` with at least |
| 241 | + |
| 242 | +```json |
| 243 | +"allowJs": true, |
| 244 | +"checkJs": true, |
| 245 | +``` |
| 246 | + |
| 247 | +4. Do "type linting" via `tsc` |
| 248 | +5. Have `tsconfig.build.json` with at least |
| 249 | + |
| 250 | +```json |
| 251 | +"declaration": true, |
| 252 | +"allowJs": true, |
| 253 | +"checkJs": true, |
| 254 | +"emitDeclarationOnly": true, |
| 255 | +``` |
| 256 | + |
| 257 | +6. Generate Types via `tsc -p tsconfig.build.types.json` |
| 258 | +7. Publish your `*.js` AND `*.d.ts` files |
| 259 | + |
| 260 | +Ideally doing the type linting happens in a `pre-commit` hook and generating the `*.d.ts` files happens in the ci for publishing. |
| 261 | +We have exactly this setup at [open-wc](https://github.com/open-wc/open-wc) and it served as well so far. |
| 262 | + |
| 263 | +Congratulations you now have a type safety without a build step :tada: |
| 264 | + |
| 265 | +#### To sum it all up - why are we fans of TypeScript even though it requires a build step? |
| 266 | + |
| 267 | +It comes down two 2 things |
| 268 | + |
| 269 | +- Typings can be immensely useful (type safety, auto complete, documentation, ...) for you and/or your users |
| 270 | +- TypeScript is very flexible and supports types for "just" JavaScript as well |
| 271 | + |
| 272 | +Follow us on [Twitter](https://twitter.com/openwc), or follow me on my personal [Twitter](https://twitter.com/dakmor). |
| 273 | +Make sure to check out our other tools and recommendations at [open-wc.org](https://open-wc.org). |
| 274 | + |
| 275 | +Thanks to [Benny](https://dev.to/bennypowers) and [Lars](https://github.com/LarsDenBakker) for feedback and helping turn my scribbles to a followable story. |
0 commit comments