From 84e3acc48626d257af6af8678eb2ca4579885f9d Mon Sep 17 00:00:00 2001 From: Matt Pocock Date: Tue, 9 Apr 2024 09:56:50 +0100 Subject: [PATCH] More reviews of 14 --- book-content/14-types-you-dont-control.md | 2 - .../chapters/14-configuring-typescript.md | 211 +++++++++++------- book-content/chapters/15-cjs-vs-esm.md | 2 - 3 files changed, 134 insertions(+), 81 deletions(-) diff --git a/book-content/14-types-you-dont-control.md b/book-content/14-types-you-dont-control.md index bfae29b..0befb55 100644 --- a/book-content/14-types-you-dont-control.md +++ b/book-content/14-types-you-dont-control.md @@ -95,8 +95,6 @@ When TypeScript transpiles this code, it will only transform the syntax– in th API methods are not polyfilled or transformed in any way, and are left as is. This could lead to runtime errors if the code is run in an environment that doesn't support these features. In this case, `replaceAll` will not be transformed, and will throw an error in an ES5-only environment that doesn't support it. -If you need to support older environments, you'll need to find your own polyfills or other tooling since TypeScript won't do it for you. - As mentioned before, it's a good idea to keep your `target` and `lib` options in sync. This ensures that you're only using features that are available in your target environment and helps avoid potential issues with unsupported APIs. ### `skipLibCheck` diff --git a/book-content/chapters/14-configuring-typescript.md b/book-content/chapters/14-configuring-typescript.md index 7cc298d..8f689ed 100644 --- a/book-content/chapters/14-configuring-typescript.md +++ b/book-content/chapters/14-configuring-typescript.md @@ -1,12 +1,10 @@ - - # 15. Configuring TypeScript -We've dipped into TypeScript's `tsconfig.json` configuration file a few times in this book. Let's go deep, and give you a full understanding of what you can do with it. +We've dipped into TypeScript's `tsconfig.json` configuration file a few times in this book. Let's take a deeper look. We won't cover every option in `tsconfig.json` - many of them are old and rarely used - but we'll cover the most important ones. ## Recommended Configuration -To start, here's a recommended base `tsconfig.json` configuration with options appropriate for any application you're building: +To start, here's a recommended base `tsconfig.json` configuration with options appropriate for most applications you're building: ```json { @@ -24,14 +22,14 @@ To start, here's a recommended base `tsconfig.json` configuration with options a } ``` -We've seen most of these settings before, but it wouldn't hurt to do a quick rundown of what each option does: +Here's what each setting does: -- `skipLibCheck`: Skips type checking of declaration files, which improves compilation speed. -- `target`: Specifies the ECMAScript target version for the compiled JavaScript code. Targeting `es2022` provides access to the most recent JavaScript features. +- `skipLibCheck`: Skips type checking of declaration files, which improves compilation speed. We covered this in the previous chapter. +- `target`: Specifies the ECMAScript target version for the compiled JavaScript code. Targeting `es2022` provides access to some relatively recent JavaScript features - but by the time you read this book, you might want to target a newer version. - `esModuleInterop`: Enables better compatibility between CommonJS and ES modules. - `allowJs`: Allows JavaScript files to be imported into the TypeScript project. - `resolveJsonModule`: Allows JSON files to be imported into your TypeScript project. -- `moduleDetection`: The `force` option tells TypeScript to treat all `.ts` files as a module, instead of a script. +- `moduleDetection`: The `force` option tells TypeScript to treat all `.ts` files as a module, instead of a script. We covered this in the previous chapter. - `isolatedModules`: Ensures that each file can be independently transpiled without relying on information from other files. - `strict`: Enables a set of strict type checking options that catch more errors and generally promote better code quality. - `noUncheckedIndexedAccess`: Enforces stricter type checking for indexed access operations, catching potential runtime errors. @@ -75,7 +73,6 @@ Based on your answers to the above questions, here's how the complete `tsconfig. "noUncheckedIndexedAccess": true, /* If transpiling with TypeScript: */ - "moduleResolution": "NodeNext", "module": "NodeNext", "outDir": "dist", "sourceMap": true, @@ -100,89 +97,60 @@ Based on your answers to the above questions, here's how the complete `tsconfig. } ``` -At this point, it would be safe for you to just skip the rest of the chapter and just use the above configuration for all of your projects. +Now that we understand the lay of the land, let's take a look at each of these options in more detail. -However, you'll gain a deeper understanding of TypeScript by seeing how these options work and why the recommended configuration is set up the way it is. +## Base Options -## Stricter Checking with `noUncheckedIndexedAccess` +### `target` -The `strict` option in `tsconfig.json` acts as shorthand for enabling several different type checking options all at once, including catching potential `null` or `undefined` issues and stronger checks for function parameters, among others. However, there isn't additional index-based checking included. +The `target` option specifies the ECMAScript version that TypeScript should target when generating JavaScript code. -This is why `noUncheckedIndexedAccess` is recommended as part of the base `tsconfig.json` configuration. When enabled, it helps catch potential runtime errors by detecting cases where accessing an array or object index might return `undefined`. +For example, setting `target` to `ES5` will attempt to transform your code to be compatible with ECMAScript 5. -Consider this example of a `VinylSingle` interface with an array of `tracks`: +Language features like optional chaining and nullish coalescing, which were introduced later than ES5, are still available: -```typescript -interface VinylSingle { - title: string; - artist: string; - tracks: string[]; -} +```tsx +// Optional chaining +const search = input?.search; -const egoMirror: VinylSingle = { - title: "Ego / Mirror", - artist: "Burial / Four Tet / Thom Yorke", - tracks: ["Ego", "Mirror"], -}; +// Nullish coalescing +const defaultedSearch = search ?? "Hello"; ``` -To accessing the b-side of `egoMirror`, we would index into its `tracks` like this: +But when they are turned into JavaScript, they'll be transformed into code that works in ES5 environments: -```typescript -const bSide = egoMirror.tracks[1]; -console.log(bSide.toUpperCase()); // 'MIRROR' +```javascript +// Optional chaining +var search = input === null || input === void 0 ? void 0 : input.search; +// Nullish coalescing +var defaultedSearch = search !== null && search !== void 0 ? search : "Hello"; ``` -Without `noUncheckedIndexedAccess` enabled in `tsconfig.json`, TypeScript assumes that indexing will always return a valid value, even if the index is out of bounds. - -Trying to access a non-existent fourth track would not raise an error in VS Code, but it does result in a runtime error: - -```typescript -const nonExistentTrack = egoMirror.tracks[3]; -console.log(nonExistentTrack.toUpperCase()); // no error in VS Code +#### `target` Doesn't Polyfill -// However, running the code results in a runtime error: -TypeError: Cannot read property 'toUpperCase' of undefined -``` +While `target` can transpile newer syntaxes into older environments, it won't do the same with API's that don't exist in the target environment. -By setting `noUncheckedIndexedAccess` to `true`, TypeScript will infer the type of every indexed access to be `T | undefined` instead of just `T`. In this case, every entry in `egoMirror.tracks` would be of type `string | undefined`: +For example, if you're targeting a version of JavaScript that doesn't support `.replaceAll` on strings, TypeScript won't polyfill it for you: ```tsx -const ego = egoMirror.tracks[0]; -const mirror = egoMirror.tracks[1]; -const nonExistentTrack = egoMirror.tracks[3]; +const str = "Hello, world!"; -// hovering over ego shows: -// const ego: string | undefined +str.replaceAll("Hello,", "Goodbye, cruel"); ``` -However, because the types of each of the tracks are now `string | undefined`, we have errors when attempting to call `toUpperCase` even for the valid tracks: - -```typescript -console.log(ego.toUpperCase()); // red squiggly line under ego - -// hovering over ego shows -'ego' is possibly 'undefined' -``` +This code will error in your target environment, because `target` won't transform it for you. If you need to support older environments, you'll need to find your own polyfills. You configure the environment your code executes in with `lib`, as we saw in a previous chapter. -This means that we have to handle the possibility of `undefined` values when accessing array or object indices. +If you're not sure what to specify for `target`, keep it up to date with the version you have specified in `lib`. -For this example, a quick way would be to add the optional chaining operator (`?.`) to the `toUpperCase` method, but depending on the context, you might want to handle `undefined` cases differently: +### `esModuleInterop` -```tsx -console.log(ego?.toUpperCase()); // no error -console.log(mirror?.toUpperCase()); // no error -console.log(nonExistentTrack?.toUpperCase()); // no error +`esModuleInterop` is an old flag, released in 2018. It helps with interoperability between CommonJS and ES modules. At the time, TypeScript had deviated slightly from commonly-used tools like Babel in how it handled wildcard imports and default exports. `esModuleInterop` brought TypeScript in line with these tools. -// running the code results in: -EGO; -MIRROR; -undefined; -``` +You can read the [release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#support-for-import-d-from-cjs-from-commonjs-modules-with---esmoduleinterop) for more details. Suffice to say, when you're building an application, `esModuleInterop` should always be turned on. There's even a proposal to make it the default in TypeScript 6.0. -Even with this simple example of logging strings to the console, the value of having `noUncheckedIndexedAccess` enabled is clear when it comes to indexing into arrays or objects. + -## The Role of `isolatedModules` +### `isolatedModules` Earlier we discussed that `moduleDetection` should be set to `force` in the `tsconfig.json` file because it tells TypeScript to treat all non-declaration `.ts` files as modules. Another recommended module-related setting to enable is `isolatedModules`. @@ -226,7 +194,102 @@ Single-file transpilation is a feature of tools like ESBuild, SWC, and Babel. Th When `isolatedModules` is enabled, TypeScript enables single-file transpilation while preventing unexpected scope issues and increasing the portability of your code across different environments. -## Module Resolution Strategies +## Strictness + +### `strict` + +The `strict` option in `tsconfig.json` acts as shorthand for enabling several different type checking options all at once, including catching potential `null` or `undefined` issues and stronger checks for function parameters, among others. + +Setting `strict` to `false` makes TypeScript behave in ways which are much less safe. Without `strict`, TypeScript will allow you to assign `null` to a variable that is supposed to be a string: + +```tsx +let name: string = null; // no error +``` + +With `strict` enabled, TypeScript will, of course, catch this error. + +In fact, I've written this entire book on the premise that you have `strict` enabled in your codebase. It's the baseline for all modern TypeScript apps. + +#### Should You Start With `strict: false`? + +One argument you often hear for turning `strict` off is that it's a good on-ramp for beginners. You can get a project up and running faster without having to worry about all the strictness rules. + +However, I don't think this is a good idea. A lot of prominent TypeScript libraries, like `zod`, `trpc`, `@redux/toolkit` and `xstate`, won't behave how you expect when `strict` is off. Most community resources, like StackOverflow and React TypeScript Cheatsheet, assume you have `strict` enabled. + +Not only that, but a project that starts with `strict: false` is likely to stay that way. On a mature codebase, it can be very time-consuming to turn `strict` on and fix all of the errors. + +So, I consider `strict: false` a fork of TypeScript. It means you can't work with many libraries, makes seeking help harder, and leads to more runtime errors. + +### `noUncheckedIndexedAccess` + +One strictness rule which isn't part of `strict` is `noUncheckedIndexedAccess`. When enabled, it helps catch potential runtime errors by detecting cases where accessing an array or object index might return `undefined`. + +Consider this example of a `VinylSingle` interface with an array of `tracks`: + +```typescript +interface VinylSingle { + title: string; + artist: string; + tracks: string[]; +} + +const egoMirror: VinylSingle = { + title: "Ego / Mirror", + artist: "Burial / Four Tet / Thom Yorke", + tracks: ["Ego", "Mirror"], +}; +``` + +To accessing the b-side of `egoMirror`, we would index into its `tracks` like this: + +```typescript +const bSide = egoMirror.tracks[1]; +console.log(bSide.toUpperCase()); // 'MIRROR' +``` + +Without `noUncheckedIndexedAccess` enabled in `tsconfig.json`, TypeScript assumes that indexing will always return a valid value, even if the index is out of bounds. + +Trying to access a non-existent fourth track would not raise an error in VS Code, but it does result in a runtime error: + +```typescript +const nonExistentTrack = egoMirror.tracks[3]; +console.log(nonExistentTrack.toUpperCase()); // no error in VS Code + +// However, running the code results in a runtime error: +TypeError: Cannot read property 'toUpperCase' of undefined +``` + +By setting `noUncheckedIndexedAccess` to `true`, TypeScript will infer the type of every indexed access to be `T | undefined` instead of just `T`. In this case, every entry in `egoMirror.tracks` would be of type `string | undefined`: + +```tsx +const ego = egoMirror.tracks[0]; +const mirror = egoMirror.tracks[1]; +const nonExistentTrack = egoMirror.tracks[3]; + +// hovering over ego shows: +// const ego: string | undefined +``` + +However, because the types of each of the tracks are now `string | undefined`, we have errors when attempting to call `toUpperCase` even for the valid tracks: + +```typescript +console.log(ego.toUpperCase()); // red squiggly line under ego + +// hovering over ego shows +'ego' is possibly 'undefined' +``` + +This means that we have to handle the possibility of `undefined` values when accessing array or object indices. + +So `noUncheckedIndexedAccess` makes TypeScript more strict, but at the cost of having to handle `undefined` values more carefully. + +Usually, this is a good trade-off, as it helps catch potential runtime errors early in the development process. But I wouldn't blame you if you end up turning it off in some cases. + +### Other Strictness Options + + + +## `module` and `moduleResolution` Speaking of modules and transpiling with different tools, the `module` and `moduleResolution` settings in `tsconfig.json` will change depending on your project. @@ -302,17 +365,11 @@ Without any `.js` files to produce, TypeScript will only have to perform type ch This is particularly useful when you have a separate build process handled by tools like Babel, webpack, or Rollup, and you want TypeScript to focus solely on type checking and linting. -In addition to being a `tsconfig.json` setting, you can also run TypeScript as a linter from the command line by using the `--noEmit` flag: - -```bash -tsc --watch --noEmit -``` - Any errors or warnings will be reported in the terminal as well as in your editor. By treating TypeScript as a linter, you'll be able to use external tools without losing type-checking and autocompletion features. -## Bundling Code for Library Use +## Transpiling Code for Library Use If you're building a library either for publishing on npm or using in your own projects, there are a few important settings you'll need to include in your `tsconfig.json` file. @@ -425,7 +482,7 @@ With this option in place, the TypeScript compiler will generate `.d.ts.map` fil Declaration maps are most useful when you're building locally in a monorepo or other project where changes to the source code directly affect the generated `.d.ts` files. If you are building a library for npm, it's likely that you won't need declaration maps since the end users will be using the built code instead of the original source. -## Configuring JSX Support +## `jsx` Recall that when building web applications, it's recommended to include `dom` and `dom.iterable` in the `lib` options of your `tsconfig.json` file: @@ -770,7 +827,7 @@ Like before, the client and server configurations would extend `tsconfig.base.js Again, this solution will work, but it might feel like clutter to have so many `tsconfig.json` files in the root directory. -### Option 3: Separate Configuration Folder +#### Option 3: Separate Configuration Folder An emerging pattern for having multiple `tsconfig.json` files in a project is to place them into a separate `.config` directory: diff --git a/book-content/chapters/15-cjs-vs-esm.md b/book-content/chapters/15-cjs-vs-esm.md index b7612b9..aeda686 100644 --- a/book-content/chapters/15-cjs-vs-esm.md +++ b/book-content/chapters/15-cjs-vs-esm.md @@ -1,5 +1,3 @@ - - # 16. CommonJS vs. ESM Module Systems The two primary module systems in the JavaScript ecosystem are CommonJS (CJS) and ECMAScript Modules (ESM).