20.2 lib: Including Type Definitions for Built-In APIs
Right, let’s talk about lib. This is the setting where you tell TypeScript, “Hey, I’m planning to run my code here. These are the built-in APIs I expect to be available.” It’s how you avoid getting errors for using Promise or Map or document.querySelector, and it’s also how you get yelled at for using fetch when you’ve told TypeScript you’re writing for Node.js 12. It’s a contract, and you get to define the terms.
Think of it this way: the TypeScript compiler itself doesn’t know a single thing about the JavaScript runtime. It doesn’t know what a window object is. It’s just a type checker. The knowledge of all the built-in JavaScript APIs—what we call the “standard library”—comes from special definition files, and the lib setting is your curated list of which ones to include.
By default, if you don’t specify lib in your tsconfig.json, TypeScript includes a sensible default based on your target. If your target is ES5, it will include lib.es5.d.ts. If your target is ES2020, it will include lib.es2020.d.ts. This is usually fine, but the moment you need something outside of that default—like DOM types for a browser project—you’ll need to explicitly set lib, and that’s where the fun begins.
What Exactly Are You Including?
When you add "lib": ["ES2020", "DOM"] to your config, you’re not adding polyfills or any actual code. You’re just telling the type checker, “Assume that the environment my code runs in will have all the APIs defined in the lib.es2020.d.ts and lib.dom.d.ts files.” This is a crucial distinction. TypeScript will now let you use Array.prototype.flatMap and document.getElementById without complaining, but it’s your responsibility to ensure the actual runtime (e.g., the user’s browser, a specific Node.js version) actually supports those features. This is why lib and target are separate settings.
The Most Common Lib Configurations
Here’s the bread and butter. For a modern web app, your config will look something like this:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"]
}
}
ES2020: Gives you types for all the JavaScript language features up to ES2020 (Promise.allSettled, optional chaining, nullish coalescing, etc.).DOM: This is the big one. It includes everything you’d expect in a browser—window,document,console,fetch,setTimeout, all of it. Forgetting this is a classic rookie mistake that results in “Cannot find name ‘console’” errors, which is a truly humiliating experience.DOM.Iterable: This is a handy supplement. It adds iterable types to key DOM structures, so you can usefor...ofloops on things likeNodeList. Without it, TypeScript will yell at you for trying to iterate aNodeListas if it were an array.
For a Node.js project, you’d leave the heavy DOM library behind and instead tell TypeScript about the Node environment.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
Wait, where are the Node-specific types like process or require? They’re not in a lib! They come from the @types/node package, which you install via npm. This is a common point of confusion. The lib setting is only for standardized JavaScript APIs (ECMAScript and the DOM). Platform-specific APIs are handled by DefinitelyTyped packages.
When Things Go Wrong: The Pitfalls
The most common error you’ll see is trying to use an API that isn’t in your lib list. TypeScript will throw a fit, saying something doesn’t exist. The solution is almost always to add the right library to your array.
A more subtle and annoying pitfall is duplication. The libraries are hierarchical. lib.es2020.d.ts includes everything in lib.es2019.d.ts, which includes lib.es2018.d.ts, and so on. Specifying ["ES2015", "ES2020"] is redundant; just "ES2020" is sufficient. Similarly, "DOM" includes most of the web-specific sub-libraries.
You can also get too specific. I’ve seen configs that list a dozen individual ES2015.* libraries. Please, for the love of your sanity and anyone else who reads your config, just use the umbrella year-based library like ES2022 unless you have a truly bizarre and specific reason not to. The TypeScript team bundles these for a reason.
The Nuclear Option: Explicitly Defining Everything
If you want absolute, granular control—or if the default inclusion is causing issues—you can set "lib": [] and then manually add every single library you need. This is the “I know exactly what I’m doing” configuration, and it’s mostly used for unique environments like cross-platform libraries or certain testing setups. For 99% of projects, it’s overkill.
{
"compilerOptions": {
"target": "ES5", // Target can be independent!
"lib": ["ES2017", "DOM", "DOM.Iterable", "ES2017.Intl"] // Manually cherry-picking
}
}
Notice here that target is set to ES5 for down-level compilation, but lib is set to ES2017, meaning we’re allowed to use modern APIs in our code (like async/await), and we’re relying on a polyfill or a bundler’s transformer to make it work in older environments. This is a perfectly valid and powerful pattern.
So, in summary: lib is your environmental assumption manifest. Set it honestly, based on where your code will live, and your relationship with the type checker will be a smooth one. Lie to it, and it will become a pedantic nuisance, constantly pointing out your fictional environment’s shortcomings.