Skip to main content

· 20 min read

Once in a while in TypeScript code, you may come across a comment that looks something like this:

ts
// @ts-expect-error
let value: number = "oops!";
ts
// @ts-expect-error
let value: number = "oops!";

You may also know that // @ts-expect-error suppresses type checking for a single line. Wanting to ignore type errors might seem counterintuitive at first. If a project already decided to use TypeScript, why disable type checking?

Rare cases do exist when TypeScript's type checking needs to be suppressed in a small area of code. TypeScript includes several "comment directives" that can change type checking behavior for a file or on a specific line. A comment directive is a comment that directs (changes) how a tool operates within a file.

This article will explain the kinds of comment directives supported by TypeScript: why they exist, what they do, and how to use them.

TypeScript's Comment Directives

TypeScript's type checker is not always correct. There are rare edge cases when its understanding of types in code doesn't perfectly match reality. For example:

  • Type definitions outside of your control might not be correct
  • Writing the correct types for some area of code might be too time consuming at the moment
  • There might be a transient bug or missing feature in TypeScript itself that prevents it from fully understanding code as written

In rare cases such as those, it can be necessary as a last-ditch resort to ignore a type error reported by TypeScript. TypeScript provides two comment directives that can ignore a type error on a line: // @ts-expect-error and // @ts-ignore.

@ts-expect-error

// @ts-expect-error is a comment directive that tells TypeScript to ignore any type errors on the next line of code. It's made to be used when TypeScript would report a type error even though code is written correctly.

Suppose you know a global process function should take in a string parameter, but its types incorrectly describe it as taking in number. If you were to call process with a value of type string, TypeScript would report a type error:

ts
declare function process(data: number): void;
 
process("abc");
Argument of type 'string' is not assignable to parameter of type 'number'.2345Argument of type 'string' is not assignable to parameter of type 'number'.
ts
declare function process(data: number): void;
 
process("abc");
Argument of type 'string' is not assignable to parameter of type 'number'.2345Argument of type 'string' is not assignable to parameter of type 'number'.

In this case, you know that TypeScript is wrong to report a type error on process("abc"). It's the types that are incorrect -- not the runtime code. You need a way to direct TypeScript to ignore that line.

Adding a // @ts-expect-error comment directive before the offending line would remove the type error:

ts
declare function process(data: number): void;
 
// @ts-expect-error
process("abc"); // No type error ✅
ts
declare function process(data: number): void;
 
// @ts-expect-error
process("abc"); // No type error ✅
caution

It is almost always better to correct type definitions rather than use comment directives. Avoiding Comment Directives covers several preferable strategies.

// @ts-expect-error is only meant to be used before lines that have a TypeScript type error. If the line after the comment does not have a type error, then TypeScript will report a new type error on the unnecessary // @ts-expect-error.

If, say, the process function's types were to be corrected to indicate data is type string, then process("abc") would no longer contain a type error. TypeScript would then report that any preceding // @ts-expect-error is unnecessary. Once a // @ts-expect-error comment directive is unnecessary, you can delete it.

ts
declare function process(data: string): void;
 
// @ts-expect-error
Unused '@ts-expect-error' directive.2578Unused '@ts-expect-error' directive.
process("abc"); // No type error ✅
ts
declare function process(data: string): void;
 
// @ts-expect-error
Unused '@ts-expect-error' directive.2578Unused '@ts-expect-error' directive.
process("abc"); // No type error ✅

@ts-ignore

// @ts-ignore is the same as // @ts-expect-error, but it is allowed to exist even if it doesn't suppress an existing type error. A // @ts-ignore above a line with no type errors will not trigger TypeScript to report a type error.

The following example places a // @ts-ignore above a process("abc") line containing no type errors. TypeScript would not produce a new type error for the unnecessary comment directive:

ts
declare function process(data: string): void;
 
// @ts-ignore
process("abc");
ts
declare function process(data: string): void;
 
// @ts-ignore
process("abc");

There are very few times when // @ts-ignore should be used instead of // @ts-expect-error. Using // @ts-expect-error instead of // @ts-ignore ensures TypeScript will report when a comment directive is no longer needed. Unnecessary comment directives are misleading and take up space.

tip

Choosing the Right Comment Directive covers the rare cases when // @ts-ignore is preferable over // @ts-expect-error.

@ts-nocheck

Many TypeScript projects are written completely or mostly with TypeScript syntax and are fully type checked. However, some TypeScript projects contain JavaScript files that haven't been structured in ways that are type-safe. Attempting to run type checking on files in those projects can sometimes result in many erroneous type errors.

Suppose a utilities.js file defines and exports a weaklyTyped object whose types are inferred incorrectly by TypeScript. Each usage of that weaklyTyped object in another file, index.ts, might incur a type error:

ts
import { weaklyTyped } from "./utilities.js";
Property 'log' does not exist on type '{}'.2339Property 'log' does not exist on type '{}'.
 
weaklyTyped.log("Hello");
weaklyTyped.log("world");
ts
import { weaklyTyped } from "./utilities.js";
Property 'log' does not exist on type '{}'.2339Property 'log' does not exist on type '{}'.
 
weaklyTyped.log("Hello");
weaklyTyped.log("world");

TypeScript provides a // @ts-nocheck comment directive that disables type checking for an entire file. Placing // @ts-nocheck at the top of a file stops TypeScript from reporting any type errors on the file.

Adding a // @ts-check at the top of the index.ts file would prevent TypeScript from reporting type errors from using weaklyTyped:

ts
// @ts-nocheck
import { weaklyTyped } from "./utilities.js";
 
weaklyTyped.log("Hello"); // No type error ✅
weaklyTyped.log("world"); // No type error ✅
ts
// @ts-nocheck
import { weaklyTyped } from "./utilities.js";
 
weaklyTyped.log("Hello"); // No type error ✅
weaklyTyped.log("world"); // No type error ✅
caution

// @ts-nocheck is even more unsafe than // @ts-expect-error and // @ts-ignore. It should only be used as a stopgap until files are refactored to have proper types.

@ts-check

// @ts-check is the opposite of // @ts-ignore: instead of disabling type checking for a file, it enables type checking for the file. This can be useful in projects where TypeScript sees JavaScript files with the allowJs compiler option and doesn't type check them with checkJs compiler option. Placing // @ts-check at the top of a JavaScript file allows easing into type checking for just that file - without enabling type checking on all files.

Take the following index.js file as an example. With TypeScript set to allowJs, it wouldn't have a type error reported for passing a string argument to a function parameter of type number. But with // @ts-check, that type error would be reported:

index.js
ts
/** @param {number} value */
function double(value) {
return value * 2;
}
 
console.log(double(1));
console.log(double("2")); // Should be a type error, but isn't ❌
index.js
ts
/** @param {number} value */
function double(value) {
return value * 2;
}
 
console.log(double(1));
console.log(double("2")); // Should be a type error, but isn't ❌
tip

See JS Projects Utilizing TypeScript in the TypeScript Handbook for using TypeScript on files not using TypeScript's syntax.

Best Practices

Comment directives are an "escape hatch": they allow switching small parts of a project to different type checking behavior. Escape hatches should be used with extreme caution and only when they are absolutely necessary.

Avoiding Comment Directives

If at all possible, don't use comment directives to suppress type errors.

Comment directives are a blunt instrument: they apply to an entire area of code. There is no way to specify a specific type error to target in comment directives.

Suppressing type errors with comment directives is a "bandaid" fix: it doesn't address whatever root issue caused the incorrect type error to begin with. When possible, it's better to improve TypeScript's understanding of code.

Consider trying the following strategies for fixing types before falling back to a comment directive.

Correcting Incorrect Types

If you know the types for code are incorrect, the best outcome is generally to fix those types. That includes declare statements as well as .d.ts files in your project and in dependencies.

In the process example from earlier, a // @ts-expect-error was used because the the declared type for process was incorrect. The // @ts-expect-error was no longer necessary once the type for process was fixed. Good!

If your project uses types from community-authored @types/ packages published by DefinitelyTyped, you may find incorrect definitions within your node_modules/. Those can be fixed at the source by sending a pull request to DefinitelyTyped. Doing so will fix the types for your project -- as well as for any other consumers of that package.

tip

The patch-package package applies changes to node_modules/ packages. You can use it to apply changes locally from in-progress DefinitelyTyped pull requests.

Refactoring for Type Safety

Type Assertions

Choosing The Right Comment Directive

Comment Directive Explanations

Comment directives such as // @ts-expect-error can also include more text. TypeScript developers will often use this to include explanations for the comment directive. Comment directive explanations are generally considered a best practice, so readers of the code understand why the comment directive is necessary.

This expanded comment from before explains the context for why process's types are wrong:

ts
declare function process(data: number): void;
 
// @ts-expect-error -- Pending 'process' being fixed to take in string.
process("abc"); // No type error ✅
ts
declare function process(data: number): void;
 
// @ts-expect-error -- Pending 'process' being fixed to take in string.
process("abc"); // No type error ✅

Including explanations with comment directives is a good practice to adhere to. In addition to explaining the "why" of the comment directive, they hint to future code authors to think on and explain any comment directive they might want to add.

Linting Comment Directives

@typescript-eslint/ban-ts-comment can be used to enforce best practices with TypeScript's comment directives. By default, the rule:

  • Always reports on @ts-ignore and @ts-nocheck comment directives
  • Reports on @ts-expect-error directives that don't have an explanatory comment description
  • Requires explanatory comment descriptions contain at least 3 characters

For example, suppose "@example/package" exports a process function that should take in a string but whose types incorrectly indicate take in a number. A // @ts-expect-error comment could be used to tell TypeScript to ignore the line:

ts
import { process } from "@example/package";
// @ts-ignore
process("New York City");
ts
import { process } from "@example/package";
// @ts-ignore
process("New York City");

Teams using issue trackers such as GitHub Issues or Jira will often prefer to include links to tracking tickets in todo comments. Doing so can help ensure the reason for the comment gets documented more fully, and isn't forgotten about later.

@typescript-eslint/ban-ts-comment's descriptionFormat option can be used to enforce comments align to a certain format. Teams can use that to enforce the explanation for a comment directive includes a link to a tracking issue.

For example, given the following ESLint config, comment directives must include a -- TODO(GH-*) linking to an issue:

ts
export default tseslint.config({
// ...
rules: {
"@typescript-eslint/ban-ts-comment": [
"error",
{
descriptionFormat: "^ -- TODO\\(GH-\\d+\\)",
},
],
},
});
ts
export default tseslint.config({
// ...
rules: {
"@typescript-eslint/ban-ts-comment": [
"error",
{
descriptionFormat: "^ -- TODO\\(GH-\\d+\\)",
},
],
},
});
ts
import { process } from "@example/package";
// @ts-expect-error -- pending updating the process types
process("New York City");
ts
import { process } from "@example/package";
// @ts-expect-error -- pending updating the process types
process("New York City");

See typescript-eslint's Getting Started guide to enable @typescript-eslint/ban-ts-comment and other recommended rules.

Equivalent lint rules in other common linters are:

Comment Directives in Other Tools

TypeScript is not the only static analysis tool that allows inline configuration comments. Formatters, linters, and others often include their own comment directives that operate separately from TypeScript.

Formatter Comment Directives

Popular web formatters such as Biome's, deno fmt, and Prettier all support comment directives that disable formatting within a file. For example, the following line taken from the Prettier documentation shows // prettier-ignore excluding a line from being formatted by Prettier:

js
matrix(1, 0, 0, 0, 1, 0, 0, 0, 1);
// prettier-ignore
matrix(
1, 0, 0,
0, 1, 0,
0, 0, 1
)
js
matrix(1, 0, 0, 0, 1, 0, 0, 0, 1);
// prettier-ignore
matrix(
1, 0, 0,
0, 1, 0,
0, 0, 1
)

Formatter comment directives don't change the behavior of TypeScript's type checker.

Linter Comment Directives

Popular web linters such as Biome's, ESLint, deno lint, and oxlint Linters such as ESLint have their own comment directives that can reconfigure the linter on a per-file or per-line basis.

For example, the following line demonstrates disabling the no-console ESLint rule for a line:

js
// eslint-disable-next-line no-console
console.log("Hello, world!");
js
// eslint-disable-next-line no-console
console.log("Hello, world!");

Linters are not the same tools as type checkers. Linter comment directives don't change the behavior of TypeScript's type checker.

For more information, see:

Closing Thoughts

Comment directives are a "necessary evil" for static analysis tools such as TypeScript. They provide an occasionally-useful escape hatch to write code that the type checker would not otherwise permit. Try to avoid comment directives if possible, and instead write code that works well with the type checker.

If you must use a comment directive, be sure to choose the appropriate one. Consider using a lint rule such as @typescript-eslint/ban-ts-comment to enforce directives be explained.


Got your own TypeScript questions? Ask @learningtypescript.com on Bluesky and the answer might become an article too!

· 20 min read

TypeScript's type system allows any two types that seem to have the same structure to be assignable to each other. But what if we want to restrict a type to only allow certain values, even if other values happen to have the same structure? Say, marking a difference between sanitized and un-sanitized strings, or positive integers from all numbers?

This need is solvable with a pattern called "branded types", sometimes also called "opaque types". Let's dive into why one might want to use branded types, how to declare and use them, and some alternatives to the pattern.

· 20 min read

One long-requested feature for TypeScript is the ability for its types to describe what exceptions a function might throw. "Throw types", as the feature is often called, are used in some programming languages to help ensure developers call functions safely.

The popular strongly typed language Java, for example, implements throw types with a throws keyword. Without reading any other code, a developer would infer from the following first line of a positive function that the function is able to throw a ValueException:

java
public static void positive(int value) throws ValueException { /* ...*/ }
java
public static void positive(int value) throws ValueException { /* ...*/ }

Throw types are also useful for developer tooling. They can tell compilers when a function call might throw an exception without safe try/catch handling.

That all seems useful, so why doesn't TypeScript include throw types?

In short, doing so wouldn't be feasible for TypeScript -- and some would argue isn't practical in most programming languages. This blog post will dig into the benefits, drawbacks, and general blockers to including throw types in TypeScript. Let's dig in!

· 20 min read

TypeScript's provides several ways to describe the type of a function that can be called in multiple different ways. But the two most common strategies -function overloads and generic functions- don't help much with how the function's internal implementation understands the types of its parameters.

This article will walk through three techniques for describing a parameter type that changes based on a previous parameter. The first two allow using standard JavaScript syntax but aren't quite precise in describing the types inside the function. The third one requires using a ... array spread and [...] tuple type in a funky new way that gets the right types internally.

· 20 min read

Most projects in the JavaScript/TypeScript ecosystem release new versions with numbers respecting semantic versioning, or semver for short. Semver is a specification that describes how to predictably increase a package's version numbers upon each new release. TypeScript is notable for not following a strict interpretation of semver for its releases. This article will dig into:

  1. What semver is and why it's useful for many packages
  2. Why following a strict interpretation of semver would be impractical for TypeScript
  3. How TypeScript's releases are versioned to an interpretation of semver that makes sense for it

While TypeScript's diverging from a common community specification can be irksome for developers, there are real reasons why it chose to diverge.

The reasoning can be summarized as:

  • Nuances of TypeScript's type checking change in virtually every release
  • It would be impractical to increase TypeScript's major version for every type checking change
  • If we consider those type checking nuances as details rather than the public API, TypeScript actually has quite good adherance to semantic versioning

This article will also more deeply explain each of those points.

· 20 min read

TypeScript is a highly configurable language. It comes with over a hundred compiler options that can be provided via the command-line to tsc and/or in a "TSConfig" configuration file (by default, tsconfig.json).

tip

TypeScript's compiler options are documented at aka.ms/tsconfig.

compilerOptions.target in particular can be an important configuration option for your project. It specifies which ECMAScript version your project's output JavaScript code must support.

You can specify target in your TSConfig as the string name of an ECMAScript version, such as "es5"or"es2021":

jsonc
// tsconfig.json
{
"compilerOptions": {
"target": "es2021"
}
}
jsonc
// tsconfig.json
{
"compilerOptions": {
"target": "es2021"
}
}

This article explores what target influences and why that's useful. Let's dig in!

· 15 min read

TypeScript 4.9 introduces a new operator, satisfies, that allows opting into a different kind of type inference from the type system's default. satisfies brings the best of type annotations and default type inference together in a useful manner. Let's explore the new satisfies operator and why it's useful!

· 20 min read

TypeScript's type narrowing is a powerful feature of TypeScript's type system that lets it infer more specific types for values in areas of code. For example, TypeScript would understand that inside the following if statement, the fruit variable has to be the literal value "apple":

ts
const fruit = Math.random() > 0.5 ? "apple" : undefined;
 
fruit;
const fruit: "apple" | undefined
 
if (fruit) {
fruit;
const fruit: "apple"
}
ts
const fruit = Math.random() > 0.5 ? "apple" : undefined;
 
fruit;
const fruit: "apple" | undefined
 
if (fruit) {
fruit;
const fruit: "apple"
}

But, TypeScript's type system isn't perfect. There are some cases where TypeScript can't narrow types exactly the way you might want.

Take a look at this code snippet, where counts.apple is inferred to be type number:

ts
const counts = {
apple: 1,
};
 
counts.apple;
(property) apple: number
ts
const counts = {
apple: 1,
};
 
counts.apple;
(property) apple: number

While counts is type { apple: number }, shouldn't TypeScript know that the immediately available value of counts.apple is specifically the literal type 1 and not the general primitive type number? Can't TypeScript tell we haven't changed the value yet?

It could, but it won't. And for very good reason.

· 20 min read

TypeScript's type system is Turing Complete: meaning it has conditional branching (conditional types) and works with an arbitrary huge amount of memory. As a result, you can use the type system as its own programming language complete with variables, functions, and recursion. Developers have pushed the bounds of type operations possible in the type system to write some pretty incredible things!

This blog post is a starting list of nifty things TypeScript developers have pushed the type system to be able to do. They range from binary arithmetic and rudimentary virtual machines to maze solvers and full programming languages.

✋ This Is Not Normal

Most applications try to get away with as few extreme type operations as possible. Complex logic in the type system gets unreadable and hard to debug pretty quickly. The Learning TypeScript book advises:

If you do find a need to use type operations, please—for the sake of any developer who has to read your code, including a future you—try to keep them to a minimum if possible. Use readable names that help readers understand the code as they read it. Leave descriptive comments for anything you think future readers might struggle with.

Please don't look at the following projects list and think you need to understand them to use TypeScript. These projects are ridiculous. They're the equivalent of code golf: a fun activity for a select few, but not useful for most day-to-day work.

· 5 min read

Most popular programming languages use the void keyword to indicate that a function cannot return a value. Learning TypeScript describes TypeScript's void keyword as indicating that the returned value from a function will be ignored. Those two definitions are not always the same! TypeScript's void comes with an initially surprising behavior: a function type with a non-void return is considered assignable to a function type with a void return.

ts
let returnsVoid: () => void;
 
returnsVoid = () => "this is fine";
ts
let returnsVoid: () => void;
 
returnsVoid = () => "this is fine";

Why is that?