---
title: 'Working With JavaScript'
source: 'https://academia.sh/en/courses/typescript/working-with-javascript'
course: TypeScript
language: en
updated: '2026-08-17T18:09:55+00:00'
license: 'CC BY-SA 4.0'
---

# Working With JavaScript

Bringing JavaScript files into the project and checking them, typing with comments, file-level check directives, and the gradual migration strategy.

The previous lesson built the way to type an untyped library from outside. The
project's **own** JavaScript files are a different problem: an existing codebase
cannot be converted overnight.

TypeScript is designed to support this migration. Every JavaScript file is at the
same time a valid TypeScript input; the level of checking can be set per project and
per file. This lesson covers the tools and order of that migration.

## Bringing JavaScript Files Into the Project

There are two separate options, and they are frequently confused.

**`allowJs`** lets JavaScript files into the project: the compiler reads them, knows
the names they export, and allows them to be imported from TypeScript files.
**`checkJs`** makes those files go through type checking.

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["src"]
}
```

Let there be a `package.json` at the project root with content
`{ "name": "measurement-migration", "type": "module", "version": "1.0.0" }`.
`src/legacy.js` is pre-migration code:

```javascript
export function average(records) {
  let total = 0;
  for (const r of records) {
    total += r.value;
  }
  return total / records.length;
}
```

`src/new.ts` uses it:

```typescript
import { average } from "./legacy.js";

const records = [{ value: 21.4 }, { value: 22.1 }];
const result: number = average(records);
console.log(result.toFixed(1));
```

With this configuration, compilation passes without error. But there is no guarantee:
the `average` function's parameter and return value count as `any`, so the `result`
declaration checks nothing.

With `"checkJs": true`:

```text
src/legacy.js(1,25): error TS7006: Parameter 'records' implicitly has an 'any' type.
```

Strict configuration now applies to the JavaScript file too. In a large codebase this
step produces hundreds of diagnostics; migration is therefore usually done file by
file.

## Typing With Comments

JavaScript files can be typed without changing their syntax. The compiler reads
comment blocks in a specific form as type declarations:

```javascript
/**
 * @typedef {object} Measurement
 * @property {string} id
 * @property {number} value
 */

/**
 * @param {readonly Measurement[]} records
 * @returns {number}
 */
export function average(records) {
  let total = 0;
  for (const r of records) {
    total += r.value;
  }
  return total / records.length;
}
```

This file checks without error under `"checkJs": true`, and the function it exports is
now typed. The TypeScript file using it:

```typescript
import { average } from "./legacy.js";

const records = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: 22.1 },
];
const result: number = average(records);
console.log(result.toFixed(1));

average([{ id: "s-03", value: "23.0" }]);
```

```text
src/new.ts(10,24): error TS2322: Type 'string' is not assignable to type 'number'.
```

The diagnostic shows that the contract coming from the comment in the JavaScript file
is genuinely enforced. The `@typedef`, `@param`, and `@returns` directives are the
comment-form counterpart of the type notations built in the course; union, array, and
generic notations are supported too.

The cost of this method is readability: the same information is written longer and
sits apart from the body. Its gain is that the file is still an ordinary JavaScript
file — it runs without needing a build step. It is a suitable middle ground in
projects with no build step, or in migration's early stage.

## File-Level Check Directives

Checking can also be turned on and off with a comment placed at the top of a file.
While `"checkJs": false`, writing `// @ts-check` in a file brings only that file under
checking:

```javascript
// @ts-check

/**
 * @typedef {object} Measurement
 * @property {string} id
 * @property {number} value
 */

/**
 * @param {readonly Measurement[]} records
 * @returns {number}
 */
export function average(records) {
  let total = 0;
  for (const r of records) {
    total += r.id;
  }
  return total / records.length;
}
```

```text
src/legacy.js(16,5): error TS2322: Type 'string' is not assignable to type 'number'.
```

The reverse directive is `// @ts-nocheck`, and it excludes a file from checking while
`"checkJs": true`.

The two directives are used at different points. `// @ts-check` is for bringing a
small number of files under checking at the start of a migration — the project
setting is off, files are turned on one by one. `// @ts-nocheck` is for temporarily
leaving out the few files remaining at the end of a migration — the project setting
is on, files are exempted one by one. The second arrangement is preferred: the
default is safe, exemptions are visible.

## Single-Line Exemption

Sometimes not a whole file but a single line is the problem. There are two directives
and the difference between them matters.

`// @ts-ignore` suppresses all diagnostics on the next line and stays silent even if
there is nothing to suppress. `// @ts-expect-error`, instead, says it **expects** a
diagnostic on the next line; if the expected diagnostic does not appear, it produces a
diagnostic of its own:

```typescript
import { average } from "./legacy.js";

const records = [
  { id: "s-01", value: 21.4 },
  { id: "s-02", value: 22.1 },
];
const result: number = average(records);
console.log(result.toFixed(1));

// @ts-expect-error string value is not accepted
average([{ id: "s-03", value: "23.0" }]);

// @ts-expect-error this line is actually valid
average([{ id: "s-04", value: 24.0 }]);
```

```text
src/new.ts(13,1): error TS2578: Unused '@ts-expect-error' directive.
```

The directive on the tenth line is correct and produces no diagnostic; the directive
on the thirteenth line is unnecessary and gets reported.

This behavior makes `// @ts-expect-error` superior to `// @ts-ignore`. When a
suppressed error is later fixed — the library gets updated, the type definition
improves — `@ts-ignore` stays silent and no one notices. `@ts-expect-error` instead
says "I am no longer needed." Exemptions clean themselves up this way.

A reason is written next to both directives; as in the example above, the rest of the
directive's text is treated as a comment.

## Gradual Migration Order

The tools gathered can be put into a sequence. Each step preserves the guarantee the
previous step provided:

1. **`allowJs` is turned on, `checkJs` stays off.** JavaScript files enter the
   project; no new diagnostic appears. Newly written code is TypeScript.
2. **Contracts for frequently used modules are written.** Either with a `.d.ts` file
   or with comments. The most-imported modules are handled first; each one spreads a
   guarantee to every file that uses it.
3. **Files are brought under checking one by one with `// @ts-check`.** Diagnostics
   that appear are fixed. Ones that cannot be fixed are justified with
   `// @ts-expect-error`.
4. **`checkJs` is turned on, remaining files are exempted with `// @ts-nocheck`.**
   The default is now on the safe side.
5. **Files are moved to the `.ts` extension** and comment types are converted to
   syntax.
6. **Strictness flags outside the umbrella are turned on one by one.** The
   `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` options from the
   Compiler Configuration lesson come at this stage.

The principle behind the order is **keeping red at zero** at every step: the build
should always pass clean, new diagnostics should not be allowed to accumulate.
Otherwise the diagnostic list turns into noise and real errors get lost in it.

At some point `any` becomes unavoidable. The rule from the escape hatches lesson
applies here: `any` is a transition marker. Where it remains is documented and
narrowed in later rounds of the migration.

## Summary

- `allowJs` brings JavaScript files into the project, `checkJs` puts them through type
  checking; the two are separate decisions.
- Comment notation (`@typedef`, `@param`, `@returns`) types JavaScript files without
  changing their syntax; the contract is enforced on the TypeScript side too.
- `// @ts-check` and `// @ts-nocheck` turn checking on and off at the file level; the
  arrangement where exemption is visible is preferred.
- `// @ts-expect-error` produces `TS2578` when the expected diagnostic does not
  appear, cleaning up unneeded exemptions; `// @ts-ignore` does not do this.
- Gradual migration proceeds by keeping the build clean at every step: bringing files
  in, writing contracts, turning on checking, moving the extension, raising
  strictness.

## Next Step

The import line in this lesson wrote `./legacy.js`, and the extension was required.
The same notation does not change even when the source is a TypeScript file — this is
one of module resolution's rules and is surprising at first. The next lesson covers
how the compiler turns a module name into a file, path mapping, and the effect of
resolution settings on output.
