20.5 include, exclude, and files: Controlling What Gets Compiled
Right, let’s talk about the three directives you’ll use to tell the TypeScript compiler, “No, not that file. I know it has a .ts extension, but trust me, it’s a hot mess and we don’t go near it.” This is your project’s bouncer, and include, exclude, and files are the guest list.
First, a crucial piece of context that everyone misses: the defaults. If you don’t specify an include array, TypeScript will, by default, grab every .ts, .tsx, .d.ts file it can find. And it will look everywhere, including node_modules (which, let’s be honest, is a digital haunted house you should never, ever compile). This is why an empty tsconfig.json is practically a declaration of chaos. You’re letting the compiler run wild in your pantry.
The Default Madness and Why You Must Tame It
To stop this madness, you define include. The moment you do, you completely override the default behavior. The compiler becomes a lot more polite and only looks where you tell it to. The exclude list then becomes a way to make exceptions within your include directories.
Think of it like this: include says, “Search in these neighborhoods.” exclude says, “But avoid these specific houses, they’re trouble.” If you don’t specify any neighborhoods (include), the compiler assumes it has a warrant to search the entire city.
Here’s the most common, sane setup. You want your source code, probably in src, and you want to ignore the output build directory to avoid an infinite loop of self-compilation (a classic rookie mistake).
{
"compilerOptions": {
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
The **/* is a glob pattern meaning “any subdirectory and any file within it.” So src/**/* means “all files in src and any of its subfolders, to any depth.”
The Special Case of files
Then there’s the oddball: files. This isn’t a list of directories; it’s an explicit, hand-crafted list of individual files. It’s like sending the compiler a specific invitation for each guest instead of saying “everyone in this area code.”
You will almost never use this. It’s a maintenance nightmare for any project larger than a “hello world.” Its primary use case is when you have a truly bizarre project structure that defies all glob patterns, or for compiling a very specific, small set of entry points. I’ve seen it used effectively in large monorepos for generating type declarations for a single package. That’s about it.
{
"files": ["src/index.ts", "src/very-special-utils.ts"]
}
If you use files, include and exclude are ignored. It’s a total takeover.
The Subtle Pitfalls of exclude
Here’s where people get tripped up. exclude only subtracts from the include list. It does not add to it. This seems obvious, but it leads to the most common head-scratcher:
{
"include": ["src/**/*"],
"exclude": ["tests"]
}
You might think, “I’ve excluded my tests directory, I’m safe.” But what if your src directory also has a folder named tests? Guess what? src/tests is still included because your include glob (src/**/*) covers it, and your exclude of tests only applies to a tests directory at the project root, not one nested inside src. To exclude that, you need to be explicit: "exclude": ["tests", "src/**/tests/**/*"]. Annoying, but true.
Another critical note: some directories are excluded by default, even if you don’t list them: node_modules, bower_components, jspm_packages, and your outDir (if specified). You only need to add them to exclude if you’ve done something truly weird, like include the node_modules directory. Please don’t do that.
The Order of Operations and @types
Let’s get into the weeds, because that’s where the bugs live. The processing order is:
- Gather all files implied by
include. - Remove any files matched by
exclude. - Remove any files that have already been found as a root of a previous file (a weird edge case for triple-slash references).
- Remove any files that are duplicates from the
fileslist.
But wait, there’s more! What about those lovely types from @types packages? Those are automatically included if the file they’re meant to type is included. So if you include a file that uses jest, the types from @types/jest are pulled in, even though they live in node_modules (which is normally excluded). The compiler is smart enough to make this one exception. It’s magic, but it’s the good kind.