Sharing TypeScript configuration
This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:12] Now that we have our monorepo setup, we can start sharing configuration. The first thing that I want us to unify across our three applications is our TypeScript configuration.
[00:13 - 01:02] Starting in the Monorepo root, I'm going to open our pnpm-workspace.yaml file. I'm going to add another folder glob under our packages option and we're going to name it "packages". This naming scheme where you have your user facing applications under "apps" and your shared code under "packages", is fairly standard in monorepos, and you would see that across the whole ecosystem. I'm going to create the packages folder and the config folder inside it. Then I'm going to cd into that folder and create a very simple package.json that just has a "name", a "version", and a "private" field.
[01:03 - 01:15] For the name of our config package, I'm going to use @monorepo/config. Shared packages in a monorepo should be under a namespace.
[01:16 - 01:23] In our case we use the @monorepo namespace. The choice of a namespace is a matter of preference.
[01:24 - 01:37] @monorepo is one commonly used name, that you might find across the ecosystem, for internal packages. One other common option is to use the name of your company.
[01:38 - 01:52] So if your company was called Meta, you might be using at @meta/config. The one thing I would strongly recommend you never do is use an existing public namespace.
[01:53 - 02:17] For example, ESLint has a public namespace on npm used for @eslint/js and other packages. You should never use one of those public namespaces because it's going to get hard to disambiguate what's a public package published on npm, and what's an internal package in your own monorepo.
[02:18 - 02:36] So now that we have created our package.json file, we can start adding dependencies to this workspace. Since we're going to be unifying our TypeScript configuration, obviously the first dependency that we're going to add is TypeScript itself.
[02:37 - 02:56] I'm going to do "pnpm add -D" (for dev dependency) "--save-exact" (to get an exact version number), and then "typescript". Next I'm going to create a file called tsconfig.base.json.
[02:57 - 03:13] This is going to be the file that we are going to extend from in all our other projects. I have prepared the config ahead of time and I'm going to paste it in here. It's based on Matt Pocock's excellent TSConfig Cheat Sheet.
[03:14 - 03:50] The link will be added to the course materials and I highly recommend reading it in your own time. I've also added comments to each option in our TSConfig, so that you don't need to go back and reference what each option means, but you have the information inline. The most important consideration for our base config file is that it is set up for type checking only. We're not going to be using TypeScript to compile any of our apps, and later on, we're also not going to use it to compile any of our library or shared code.
[03:51 - 03:59] It's only going to be used for type checking. The other important consideration in this base config is the "lib" option.
[04:00 - 04:19] This option defines which ECMAScript language features are supported by TypeScript, and whether browser bindings like the DОМ will be available and recognized. In our base config, we're only going to set a library version of ES2022.
[04:20 - 04:32] For other configs that do need browser types, we're going to override this option and include the DOM types. But here in the base one we're just going to stick to ES.
[04:33 - 05:17] The reason we are using ECMAScript 2022 instead of 2023 2024 or ESNext, is that at the time of recording of this course, which is late November 2024*, Firefox still does not fully support any higher version. There are some minor incompatibilities with ES2023 and ESNext in Firefox, so to stick to things that are guaranteed to work, we're just going to use ES2022. Maybe by the time you're consuming this course, those incompatibilities have already been fixed, and you can opt into ES2023 or ES2024.
[05:18 - 05:40] I still would recommend always sticking to a particular version and avoiding ESNext. Now that we have our tsconfig.base.json file setup in our @monorepo/config package, I'm going to go back to the root of the monorepo and I want to look at a tree representation of our packages folder.
[05:41 - 06:02] We see that includes the config folder and that it has its own modules, which we don't care about. The parts that we care about, it is that it has a package.json and a tsconfig.base.json. For the purposes of Node, any folder that has package.json is a Node Module.
[06:03 - 06:33] That means that when we install this Node Module in other parts of our system, it's going to have access to basically these two files to the package.json, which is always necessary for resolving what a module is and to our tsconfig.base.json. I'm now going to go into our frontend-nextjs app, and then I'm going to add a dev dependency on @monorepo/config.
[06:34 - 06:55] I'm going to do this with "pnpm add -D" (for dev dependency) and then "@monorepo/config:workspace@^". This means use the workspace protocol at version caret, which basically means use the current latest version.
[06:56 - 07:51] I could have also used "pnpm add -D --workspace @monorepo/config" without specifying the protocol manually, and the result would have been exactly the same. But I think that this is more instructive in terms of how this dependency is added. Now that I have added the dependency on our config package, we can open Next.js' TSConfig.json and we can start seeing which options are duplicated and which are different between our base config and Next.js' default config. I'm going to extend our base config and then open it in a pain on the right side of my editor. And then we can go through the options one by one and update them as necessary. The first one that we need to look at is the "lib" option.
[07:52 - 08:22] As we mentioned before, our base config does not contain DOM types, it just contains the ES lib types. So we do need to keep the DOM types in our Next.js config, but we should change the ESNext lib version to ES2022 for the reasons that we discussed previously. Next, I'm going to delete all duplicated options that are already set to the correct values in our base config.
[08:23 - 08:35] Then we come up to the JSX option. Our base config does not contain this option because it's specific to projects with React or similar front end frameworks.
[08:36 - 08:54] When JSX is set to "preserve", TypeScript can correctly parse JSX, but it's not going to try to transpile it in any way. This is because Next.js only uses TypeScript for type checking, and it does transpilation with Webpack and Turbopack.
[08:55 - 09:24] And just for posterity, when I say transpilation, what I mean is converting our JSX and TypeScript into JavaScript that's fit for browsers. We're going to go into more details how this works later in the course, but suffice to say you cannot keep JSX as is. It needs to be converted into JavaScript function calls to work correctly.
[09:25 - 09:38] The next option in the config is "incremental". What this means is that in large projects, TypeScript is going to keep some cache information between type checking runs.
[09:39 - 09:58] This may speed up type checking for very large projects, and it's the default that Next.js ships with, so we're just going to keep it as is. The next option is plugins, and obviously we do want the Next.js TypeScript plugin to be enabled for this project, so we're going to keep it.
[09:59 - 10:06] Finally there is the "paths" option. Paths allow you to rewrite imports in TypeScript.
[10:07 - 10:41] We will discuss more about path rewriting and the alternative to TypeScript paths with package.json imports later in the course. Suffice to say that in this case, it allows you to require any file in the repository starting with the "@" symbol followed by a slash, and then you can reference a file as if you're starting from the root of the project, rather than having to do a relative traversal from your current folder.
[10:42 - 10:59] I'm going to go back to the command line, and I'm going to run the type:check script just to verify that everything works correctly with our changes. And it does. Then I'm going to go to our second app, frontend-vite, and I'm going to go to the same process.
[11:00 - 11:23] I'm going to add a dev dependency on "@monorepo/config@workspace:^". And then if we look at the list of TSConfig files in Vite's directory, we're going to see that it's doing something a bit more complicated with its TypeScript setup. It has three TSConfig files.
[11:24 - 11:32] This is because Vite uses something called a composite project. Let's open the first TSConfig file and see what we have in it.
[11:33 - 11:58] The first TSConfig file is a root TSConfig file with an empty configuration that sets no behavior on its own. The only thing it does is work as an entry point for two other config files. tsconfig.app.json and tsconfig.node.json. Let me open those side by side.
[11:59 - 12:16] We have tsconfig.app.json on the left side and tsconfig.node.json on the right side. They are very similar, with the main difference being that the app one is configured for a browser while the node one is not.
[12:17 - 12:38] The other very important difference is what they have in their "include" section. The app file matches all files in the "src" source directory, while the node file matches only a single vite.config.ts root configuration file.
[12:39 - 13:02] This means that type checking is done one way for normal application files, and then slightly differently for our config file. This is actually a slightly better way of doing things compared to what Next.js is doing, but as you can see, it's also more complex.
[13:03 - 13:22] In any case, we're not going to change fundamentally what Vite is doing here, but we're going to extend both of these config files with our base file and then remove all unnecessary options. So let me do that first for the tsconfig.app.json file.
[13:23 - 13:45] After we extend from our base config, the only options we need to keep are the "lib" option and the "jsx" option. We are going to set the same ES2022 version that we have in our base config because as we discussed, this is safer and we're going to keep the "jsx" option unchanged.
[13:46 - 14:02] As for tsconfig.app.json, we can actually remove all compiler options. Our base config already covers everything necessary, so we don't need to do any overrides in this case.
[14:03 - 14:16] I'm going to save the files, drop down to the command line, run "type:check" and verify that everything works correctly. And it does. We need to pause here because I made a mistake.
[14:17 - 14:37] There is a very weird behavior that can be exhibited if we try to build this app. It's going to fail with an "Import path can only end with a ".tsx" extension when 'allowImportingTsExtensions' is enabled" error and this error only shows up on build and not on type:check, so that's why I missed it.
[14:38 - 14:47] The fix is straightforward enough. We're going to go into our tsconfig.app.json file and we're going to enable that option.
[14:48 - 14:56] Then we can check that, yeah, type checking still passes and build now also works. Sorry about the mistake. Back to the original video.
[14:57 - 15:10] This is two apps down. One more to go. I'm going to go into our server-express application and I'm going to go to the same process.
[15:11 - 15:40] I'm going to add our dev dependency on "@monorepo/config@workspace:^" I'm going to edit the tsconfig.json, extend from our base config, and finally verify that everything works with the type check and it does. Finally, I want to go back to the root of the monorepo and then I want to run "type:check" for all apps from here.
[15:41 - 16:09] We can see that it all passes and I can add my changes to git and we can look at the diff very briefly. We can see that our @monorepo/config package is added to dependencies of all three apps, and we can see how short the new TSConfig files are, now that we have our common base to extend from. This wraps up this lesson.