Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

How can you type-check JavaScript with TypeScript?

Questions in this chapter (6)
  • Draft
  • easy
  • Askedsometimes
  • Verified October 2026

Answer

tsc can type-check JavaScript because it understands JSDoc comments as type annotations. You opt in at one of three levels:

  • Per file: put // @ts-check on the first line. Other .js files in the project stay unchecked.
  • Whole project: set allowJs (include .js files in the program) and checkJs (report errors in them). Per the docs, checkJs is the same as adding // @ts-check to every JavaScript file.
  • Opt out: // @ts-nocheck skips a file; // @ts-ignore and // @ts-expect-error silence the next line, and the latter errors when the line is no longer wrong (TS2578).

Annotating. @param {string} name, @param {number} [times] for optional, @returns {string}, @type {Foo}, @typedef, @template T for generics, @satisfies, and casts such as /** @type {number} */ (x). Types from another file come in through @import {Pet} from "./types" (TS 5.5) or import("./types").Pet. These tags exist only for the checker; they are not imported at runtime.

Why do it. It adds checking and editor help to a JavaScript codebase without a build step, which fits gradual migration or small scripts. JSDoc types also feed declaration generation: with allowJs, declaration, and emitDeclarationOnly, tsc produced shout(name: string, times?: number): string from the comments below.

Differences from .ts. Unannotated parameters are any unless noImplicitAny is on, and function parameters are optional unless JSDoc says otherwise (Handbook).

Version note (TS 7). The 7.0 announcement says JavaScript support was reworked to match .ts analysis: @enum, @class constructors, a bare ? type, and Closure-style function(string): void are no longer recognised.

Example

// src/ex.js  (TypeScript 7.0.2 and 5.9.3: tsc --allowJs --noEmit)
// @ts-check
/** @typedef {{ id: number, tags?: string[] }} Item */

/**
 * @param {string} name
 * @param {number} [times]
 * @returns {string}
 */
export function shout(name, times = 1) {
  return name.toUpperCase().repeat(times);
}
/** @param {Item} item */
export const label = (item) => `#${item.id}`;

shout(42);
// Error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
label({ id: "7" });
// Error TS2322: Type 'string' is not assignable to type 'number'.

Run unchanged on Node 24.19.0, the same file throws TypeError: name.toUpperCase is not a function at shout(42): the comments are checked at build time and ignored at runtime.

Pitfalls

  • Setting checkJs without allowJs. checkJs works in tandem with allowJs; the docs describe them as a pair.
  • Expecting // @ts-check in one file to check the others. With only allowJs, only the file with the comment reported an error in my run (both versions).
  • Assuming @ts-expect-error is a harmless ignore. It reports TS2578 when the next line is fine, so stale suppressions surface.
  • Carrying old JSDoc forward to TS 7. The same project that was clean on 5.9.3 reported four errors on 7.0.2 for @enum (TS2749), a {?} type (TS1110), and function(string): void (TS1005 plus an implicit-any TS7006 on the callback).

Follow-ups

Does noImplicitAny apply to JavaScript?
Yes under checkJs. function add(a, b) reported TS7006 under --strict on both versions. Without --strict it was clean on 5.9.3 but an error on 7.0.2, because 7.0 turns strict on by default (7.0 announcement).
Where does the type information for a plain-JS library come from?
Usually a .d.ts (see the declaration-files question). maxNodeModuleJsDepth can make tsc infer types from JavaScript inside node_modules, but the docs advise leaving it at 0.