20.8 Extending tsconfig: Shared Base Configurations
Right, so you’ve got more than one project. And you’re not a masochist. You don’t want to copy-paste the same 50 lines of tsconfig.json into every single frontend app, library, and random script folder. This is where extends swoops in to save you from your own maintenance nightmare. It’s the closest thing we have to configuration inheritance, and it’s glorious.
The concept is simple: you define a base tsconfig.json file with all your shared, common settings. Then, in your individual project configs, you point to that base file and add the project-specific tweaks. TypeScript will effectively merge the two. It’s like a parent setting the house rules (“no anys in this house, young man!”), and the child project adding its own exceptions (“but I need "moduleResolution": "nodenext" for this one thing!”).
How to Structure Your Base Config
First, create a base configuration. The convention is to name it something like tsconfig.base.json to make its purpose clear. You’ll want to put all the universal settings here—the stuff every single one of your TypeScript projects should use.
// tsconfig.base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler", // Because you're probably using a modern bundler, right?
"strict": true, // Non-negotiable. Turn this on.
"esModuleInterop": true,
"skipLibCheck": true, // A performance godsend for big monorepos
"forceConsistentCasingInFileNames": true,
"declaration": true, // Crucial for libraries
"declarationMap": true,
"allowSyntheticDefaultImports": true
}
}
Notice what’s not in here: include, exclude, or files. Those are almost always project-specific. Your base config is for compiler rules, not for defining a project’s footprint.
Extending from the Base
Now, in your actual project, your tsconfig.json becomes beautifully lean.
// apps/cool-app/tsconfig.json
{
"extends": "../../tsconfig.base.json", // Relative path to the base config
"compilerOptions": {
"outDir": "./dist", // Project-specific setting
"jsx": "react-jsx" // This app needs React settings
},
"include": ["src/**/*"] // Define the project's source here
}
The magic is in the merge algorithm. It’s not a dumb overwrite. The child compilerOptions object is merged on top of the parent’s. If you specify the same option in both, the child’s value wins. This is how you override the defaults for a specific project.
The Gotchas (Because Of Course There Are Gotchas)
Relative Paths are from the Child Config: This is the biggest “aha!” moment for people. The path you put in the
extendsfield is relative to the config file it’s written in. If your childtsconfig.jsonis inapps/cool-app/and your base is in the repo root, you use"extends": "../../tsconfig.base.json".You Can Extend from
node_modules: This is a killer feature for sharing configs across multiple repositories. If you publish a package@myorg/tsconfig-base, you can extend it directly:{ "extends": "@myorg/tsconfig-base", // ... your overrides }TypeScript will resolve this just like it resolves an npm module. The package just needs to have the config file as part of its published contents.
It’s a Merge, Not an Override: I said it before, but it’s worth repeating. If your base sets
"strict": trueand a child sets"strict": false, you’re now the proud owner of a non-strict project. The child wins. This is usually what you want, but don’t accidentally relax your rules by blindly copying a config snippet.You Can Extend Multiple Levels: Your config can extend a base, which can extend another base. Don’t go too crazy with this—configuration Inception is a real pain to debug. I usually recommend a single base for a whole repository or set of projects. If you need more complexity, you’re probably better off with a few specialized bases (e.g.,
tsconfig.react.base.json) that your projects can extend directly.
Why This is the Only Sane Way to Work
Beyond the obvious “Don’t Repeat Yourself” benefits, a shared base config enforces consistency. It’s the single source of truth for your compiler rules. When you decide to finally enable exactOptionalPropertyTypes (you brave soul), you can do it in one place and watch the beautiful errors light up across every project simultaneously. It’s not just convenient; it’s a critical tool for scaling TypeScript across anything larger than a single-project hobby codebase. Use it.