skip to content

allowJs, checkJs & JSDoc Typing

You will learn to admit .js files into the program, turn the checker on for them selectively, and express real types in JSDoc comments without changing a line of syntax. Interviewers ask because JSDoc typing is how you get type safety in a codebase that cannot adopt .ts files yet.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In a tsconfig.json, what does the TypeScript compiler option `allowJs` do on its own, and what changes when you also turn on `checkJs`?

level: juniorimportance: must knowfreq 58%

answer

  1. two flags, two different jobs
  2. one admits files, the other checks them
  3. inference happens either way
  4. emit means output can hit input
  5. per-file comments override the project switch

basics

~20 s

allowJs admits .js and .jsx files into the TypeScript program so they are resolved, inferred from and emitted — but no errors are reported in them. checkJs additionally turns the type checker on for those JavaScript files.

solid answer

~50 s

By default `tsc` only puts `.ts`, `.tsx` and `.d.ts` files into the program. `allowJs` adds `.js` and `.jsx`: they become real inputs, so imports to them resolve, the compiler infers types from their code and JSDoc, and it emits them through the normal pipeline honouring `target` and `module`. What `allowJs` does *not* do is report a single type error inside them. `checkJs` is the second half — it tells the checker to report diagnostics in JavaScript files exactly as it does in TypeScript files, using whatever it can infer plus JSDoc comments. `checkJs` only means something once `allowJs` has admitted the files; there is nothing to check otherwise. You can also opt individual files in or out with the `// @ts-check` and `// @ts-nocheck` comments. One practical trap: because `allowJs` makes `tsc` emit JavaScript, you need an `outDir` distinct from your sources or `noEmit`, otherwise output would land on top of the inputs.

code

json · 11 lines
json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "outDir": "./dist"
  },
  "include": ["src"]
}

go deeper

for a junior

Know the one-line split: allowJs lets JavaScript into the compilation, checkJs makes the compiler complain about it. Say plainly that allowJs alone reports no errors.

for a middle

Explain the mechanics: the program's accepted extensions, inference from JSDoc and code, emit through target/module, and why an outDir separate from the sources is needed once JavaScript is an input.

for a senior

Show judgment about rollout: allowJs first so TypeScript consumers stop seeing any, then checking file-by-file with // @ts-check, and a non-emitting type-check job so a checking failure never blocks the build pipeline.

for a principal

Own the policy question — whether JavaScript stays a permanent first-class citizen of the build or is a transitional state, what the compiler is authoritative for versus the bundler, and how that choice shapes build time and CI topology.

## What "the program" is When `tsc` runs, it first builds a **program**: the set of source files it will parse and reason about. That set starts from `files`/`include` in `tsconfig.json` and grows transitively through every import it can resolve. Out of the box the program only accepts `.ts`, `.tsx` and `.d.ts`. A `.js` file sitting next to them is invisible to the compiler. That invisibility has a consequence people meet before they ever read the flag list: from a `.ts` file, `import { format } from './util.js'` finds no declaration for `./util`, so the import is an implicit `any` — and an outright error under `noImplicitAny`. ## allowJs: admission, inference, emit `allowJs: true` adds `.js` and `.jsx` to the accepted extensions. Three things follow. **Resolution.** Imports pointing at JavaScript now resolve to real source files instead of dead ends. **Inference.** TypeScript reads the JavaScript and infers types from it — literal initialisers, function bodies, and any JSDoc comments already present. Consumers do not get `any`; they get a best-effort signature. This is why `allowJs` alone is often worth turning on: your existing TypeScript gains real types for the JavaScript it calls. **Emit.** The `.js` files are now compiler *inputs*, so they are also compiler *outputs*: downlevelled to `target`, module syntax rewritten per `module`, comments and helpers handled the same way. Since inputs and outputs are both JavaScript, they can collide, and the compiler refuses rather than clobbering your source: ``` error TS5055: Cannot write file '/app/src/util.js' because it would overwrite input file. ``` The fix is an `outDir` that is not the source directory, or `noEmit: true` if another tool (a bundler, `tsx`, Babel) does the actual building and `tsc` is only your type-check gate. What `allowJs` deliberately does **not** do is report errors in those files. A `.js` file can call an undefined function or pass a string where a number is expected and the build stays green. ## checkJs: turning the checker on `checkJs: true` flips that last part: the checker now reports diagnostics in JavaScript files with the same rules it applies to TypeScript. Unresolved names, wrong argument types, property access on a possibly-undefined value under `strictNullChecks` — all of it surfaces. The rules are the same; the *information* is different. A `.js` file cannot carry type syntax — `function f(x: string)` is a syntax error in JavaScript — so everything the checker knows comes from three places: the code itself, initialisers, and JSDoc comments. Adding `@param`/`@returns`/`@typedef` is how you feed it more. `checkJs` is a project-wide switch, but checking is decided per file, and two comments override it: ```js // @ts-check // this file is checked even when checkJs is off ``` ```js // @ts-nocheck // this file is skipped even when checkJs is on ``` That pairing is what makes the flag usable on a large codebase: turn `checkJs` on and exempt the handful of files that are not ready, or leave it off and opt files in one at a time. ## Choosing a combination - **Neither flag.** A pure TypeScript project. Any stray `.js` is outside the program. - **`allowJs` only.** JavaScript participates and is emitted, TypeScript consumers get inferred types, nothing in the JavaScript is enforced. A very cheap first step. - **`allowJs` + `checkJs`.** The JavaScript is genuinely type-checked. Expect errors immediately; that is the point. - **`allowJs` + `// @ts-check` per file.** Same enforcement, file by file, with the project switch still off. The strictness flags stack on top of whichever you choose: with `strict` on, a checked `.js` file is held to `strictNullChecks` and the rest just like a `.ts` file. ## Editors The same machinery powers editors that use the TypeScript language service, which is why a plain `.js` file with `// @ts-check` at the top starts showing red squiggles in VS Code with no build step involved. The compiler flags decide what your *build* enforces; the comment decides what a single file enforces everywhere.

  • With `allowJs` on but `checkJs` off, what type does a `.ts` file see when it imports a function from a `.js` file?
    An inferred signature, not `any`. The compiler parses the JavaScript, infers parameter and return types from the body, defaults and any JSDoc, and hands that to the importer. It simply never reports errors found *inside* that JavaScript file. Without `allowJs` at all, the import has no declaration to read and degrades to an implicit `any`.
  • Right after enabling `allowJs`, `tsc` fails with "Cannot write file ... because it would overwrite input file". What happened?
    The `.js` sources are now compiler inputs, so the compiler wants to emit `.js` outputs — and with no `outDir` (or an `outDir` equal to the source root) the output path is the input path. Point `outDir` at a separate build directory, or set `noEmit: true` if `tsc` is only your type-check gate and something else builds.
  • Does turning on `checkJs` change what the compiler emits?
    No. `checkJs` is purely a diagnostics switch; emit is governed by `allowJs`, `target`, `module`, `outDir` and friends. A project can fail type-checking under `checkJs` and still produce identical output to before — which is exactly why teams sometimes run `checkJs` in a separate non-emitting check job.

saying these in an interview costs you the question

  • Says allowJs type-checks JavaScript files by itself
  • Thinks JavaScript files import as any even with allowJs on
  • Believes allowJs rewrites .js files into TypeScript
  • Expects checkJs to work without the files being in the program
  • Thinks you can write TypeScript annotations in a checked .js file

context

open as a page

In TypeScript, what is the difference between the `// @ts-expect-error` and `// @ts-ignore` comments, and why is one usually preferred?

level: middleimportance: must knowfreq 62%

basics

~20 s

Both suppress type errors on the line that follows them. // @ts-expect-error additionally requires an error to be there — if the line becomes valid, the compiler reports the directive as unused, so stale suppressions surface instead of rotting.

open as a page

In a JavaScript file that TypeScript is checking, how do you annotate a function's parameters and return type, declare a reusable named object type, and make a function generic — using only JSDoc?

level: middleimportance: must knowfreq 54%

basics

~20 s

Use JSDoc tags in comments: @param and @returns for a function's signature, @type for a variable or a cast, @typedef to declare a reusable named type, and @template to introduce a type parameter. TypeScript reads them as real types.

open as a page

What do the `// @ts-check` and `// @ts-nocheck` comments do when placed at the top of a file, and how do they interact with TypeScript's `checkJs` compiler option?

level: middleimportance: should knowfreq 46%

basics

~20 s

Placed at the top of a file, // @ts-check makes TypeScript type-check that one file even when checkJs is off, and // @ts-nocheck makes it skip that file even when checking is on. Both override the project setting per file.

open as a page

What does TypeScript's `maxNodeModuleJsDepth` compiler option control, why is its default 0, and what goes wrong when you raise it?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

maxNodeModuleJsDepth sets how many folder levels deep under node_modules TypeScript will load JavaScript files to infer types from. It defaults to 0 so dependency types come from declaration files, not from inferring over dependency source.

open as a page