After adding `"types": ["node"]` to compilerOptions in a tsconfig.json, every test file starts failing with "Cannot find name 'describe'". What does the `types` option control, and how does it differ from `typeRoots`?
answer
- it is an allow-list, not an addition
- the default includes everything it finds
- one option picks folders, the other picks packages
- globals only — imports are unaffected
- an empty array is a deliberate setting
basics
~20 sThe types option is an allow-list for the @types packages that are auto-included as globals. Naming any package suppresses the automatic inclusion of all the others, so the test framework's global declarations disappear. typeRoots changes which folders that automatic inclusion scans.
solid answer
~50 sBy default the compiler automatically includes every package it finds under `node_modules/@types` — in the current folder and every parent — and their global declarations become visible everywhere without an import. That is how `describe` and `it` exist as globals in the first place. `types` replaces that automatic set with an explicit list: `["node"]` means *only* that one, so the test framework's globals are no longer included and the names go unresolved. `typeRoots` is the other half — it changes which directories are scanned for that automatic inclusion, defaulting to every `node_modules/@types` up the tree. Both options only govern packages pulled in *implicitly*; a package you `import` explicitly resolves its own declarations regardless. The usual fixes are to add the missing entry to the list, or to give the tests their own tsconfig that includes the test-framework types while application code stays lean.
code
json · 5 lines{
"compilerOptions": {
"types": ["node", "vitest/globals"]
}
}go deeper
Recall that some names such as describe come from declaration packages included automatically, and that listing anything in the types option limits that set to exactly what you list.
Explain the split cleanly: typeRoots decides which folders are scanned for automatic inclusion, types decides which of the found packages are included, and neither affects explicitly imported packages or the emitted output.
Show judgment about ambient surface: argue for scoping test-framework globals to a test-only tsconfig so production code cannot reference them, and explain why an empty types array is a reasonable default for a published library.
Own the policy — uncontrolled ambient globals from transitive @types packages are an invisible coupling across a repo, so decide where the allow-list is defined, who may extend it, and how per-package tsconfigs inherit it.
## The implicit global inclusion nobody configures Most TypeScript projects never think about how names like `describe` or `it` become available without an import. The mechanism is automatic type inclusion: the compiler enumerates the folders in `typeRoots` — by default `node_modules/@types` in the containing directory and every parent directory, up to the filesystem root — and includes *every* package it finds there in the compilation. Those packages typically contain global declarations, so their symbols are visible in every file. This default is convenient and slightly out of control: installing any dependency that happens to pull in an `@types` package silently adds globals to your program. ## What `types` does `types` turns that default into an explicit allow-list: ```json { "compilerOptions": { "types": ["node", "vitest/globals"] } } ``` The key point — and the source of the reported failure — is that it is a **replacement**, not an addition. The moment you write `"types": ["node"]`, the automatic inclusion of everything else stops. The declaration package that provided `describe` and `it` is no longer part of the compilation, and the names become unresolved identifiers. Nothing was uninstalled; it simply stopped being included. `"types": []` is the extreme form: no automatic global inclusion at all. That is a deliberate and often good setting for a library, because it guarantees that no ambient globals leak in from a transitive dependency — for instance, browser-targeted code that accidentally compiles against server-side globals and then fails in a browser. Entries may also name subpaths, as in `"vitest/globals"`, when a package publishes its global declarations under a subpath rather than as a top-level `@types` package. ## What `typeRoots` does `typeRoots` controls *where* that automatic inclusion looks. Setting it to `["./typings", "./node_modules/@types"]` says: scan those two folders instead of the default upward walk. It is the option you reach for when hand-written declaration packages live in a project folder rather than in `node_modules`, or when you want to stop the compiler climbing out of a monorepo package into a parent's `@types`. The division of labour is clean: `typeRoots` = which folders are scanned, `types` = which of the packages found there are actually included. Restricting `typeRoots` and restricting `types` both narrow the global surface, but through different levers, and confusing them is the classic tell in an interview answer. ## What neither option touches Both apply only to *implicit* inclusion. If a file writes `import { readFile } from 'node:fs'`, the declarations for that module are resolved through ordinary module resolution and are unaffected by whether `node` appears in `types`. The distinction matters: `types` governs ambient globals, not the ability to import a typed package. That is why removing an entry can break a file that never imported anything and leave an importing file untouched. Neither option has any effect on emit. Declaration packages are erased like all types; changing these lists changes what the checker sees, never what runs. ## Fixing the reported failure Three reasonable responses, in rough order of preference: 1. **Add the entry.** If tests and application code share one compilation, list the test framework's globals alongside `node`. 2. **Separate the compilations.** Give the tests their own tsconfig that extends the base one and adds the test types, so application code cannot accidentally reference a test global. This is the answer that shows design sense — test globals leaking into production code is a real defect class. 3. **Import instead of relying on globals.** Most test frameworks also expose their API as named imports, which removes the dependency on ambient globals entirely and sidesteps the option. What is *not* a fix is deleting the `types` entry and going back to the default, if you added it for a reason — the reason was almost certainly to stop unrelated globals leaking in, and that reason still holds.
- Does the types option affect `import { readFile } from 'node:fs'`?No. `types` governs only the packages included implicitly for their global declarations. An explicit import resolves its own declarations through normal module resolution, so the import type-checks whether or not `node` appears in the list. That is why trimming the list breaks files that reference globals and leaves importing files untouched.
- What does `"types": []` accomplish in a library's tsconfig?It disables automatic inclusion of every `@types` package, so no ambient globals arrive from transitive dependencies. For a browser-targeted library that prevents server-side globals from silently type-checking, turning a runtime failure into a compile error. You then add back exactly what the code legitimately needs, which makes the dependency on ambient declarations explicit.
saying these in an interview costs you the question
- Thinks the types option lists packages npm should install
- Says types and typeRoots do the same thing
- Assumes adding an entry only adds and never removes
- Believes it also controls explicitly imported packages
- Thinks the missing globals mean the package was uninstalled