Running it

Get it running, and understand what each of the flags you are about to turn on actually does.

02of 14
≈ 10Min read
7Parts

The compiler

tsc does two things: it checks the types and it emits JavaScript. Most projects only want the first, because a bundler — Vite, esbuild, swc — does the second far faster by stripping the types without checking them at all.

two jobs, and they are separable
npm i -D typescript
npx tsc --init          # writes tsconfig.json

npx tsc                 # check, and emit .js
npx tsc --noEmit        # check only — what CI should run
npx tsc --watch         # check on save

Which means the build does not check anything

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build",
    "typecheck": "tsc --noEmit --watch"
  }
}
A type error does not stop a bundler

esbuild and swc erase types without reading them. npm run dev will happily run code full of type errors. Put tsc --noEmit in your scripts and in CI, or the checker is decoration.

tsconfig.json, the parts that matter

  • strict — the one that matters. It turns on eight flags at once, and a project without it is checking about half of what it could.
  • target — how far down the syntax is compiled. Match what you actually support.
  • lib — which built-in type definitions exist. Without "DOM", document is unknown.
  • skipLibCheck — do not type-check your dependencies’ declarations. Saves seconds, costs nothing.
  • moduleResolution: "bundler" for anything bundled; "nodenext" for Node.
a sane starting point
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM"],
    "jsx": "react-jsx",
    "noEmit": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src"]
}

What strict actually turns on

strictNullChecks is the one that finds real bugs on the first day you turn it on. Without it, door.name.trim() on a missing door is accepted; with it, you are asked what happens when it is not there.

FlagWhat it stops
noImplicitAnyA parameter with no type quietly becoming any
strictNullChecksnull and undefined being assignable to everything
strictFunctionTypesUnsound parameter substitution in callbacks
strictPropertyInitializationA class field that is never assigned
noImplicitThisthis being any in a loose function
useUnknownInCatchVariablesAssuming a caught error is an Error

Worth adding beyond strict

the four that repay the noise
{
  "noUncheckedIndexedAccess": true,   // arr[0] is T | undefined, because it might be
  "noUnusedLocals": true,
  "noUnusedParameters": true,
  "noFallthroughCasesInSwitch": true,
  "exactOptionalPropertyTypes": true  // { a?: string } is not the same as { a: string | undefined }
}
On a new project, turn them all on now

Adding a strictness flag to a young codebase is an afternoon. Adding one to a two-year-old codebase is a quarter.

Types for packages

three situations
import express from "express";      // ships its own types — nothing to do
npm i -D @types/node               // types published separately
// and for a package with none at all:
// src/types/untyped-thing.d.ts
declare module "untyped-thing";     // everything from it is any, but it imports

Reading an error

A TypeScript error is a chain of reasoning: the first line is the conclusion and the last is the cause. Read from the bottom. For deep generic errors, ts-error-translator in the editor turns the worst of them into English.

start at the bottom
Argument of type '{ name: string; }' is not assignable to parameter of type 'Door'.
  Property 'days' is missing in type '{ name: string; }' but required in type 'Door'.
//  ↑ the second line is the actual problem

Practice