The basic types

Annotate what needs it, let inference do the rest, and know the difference between any and unknown.

03of 14
≈ 11Min read
8Parts

Let it infer

Annotate the inputs and outputs — parameters, return types, exported constants — and let everything in between be inferred. An annotation that repeats what the compiler worked out is one more thing to keep in step.

most annotations are noise
const name: string = "Painted";   // the compiler already knew
const name = "Painted";           // type: "Painted"
let other = "Painted";            // type: string

const days = 5;                   // 5
const doors = ["a", "b"];         // string[]
const pair = { name: "Painted", days: 5 };   // { name: string; days: number }
const narrows, let widens

const name = "Painted" has the literal type "Painted", because it can never be anything else. let widens to string, because it can.

The basic types

lower case: the primitives
let s: string;
let n: number;          // one number type: no int, no float
let b: boolean;
let big: bigint;
let sym: symbol;
let nothing: null;
let missing: undefined;

// NOT String, Number, Boolean — those are the wrapper objects, and never what you want

Arrays, and the two ways to write them

they are the same type
const names: string[] = [];
const names: Array<string> = [];

const doors: Door[] = [];
const grid: number[][] = [];
const mixed: (string | number)[] = [];
const readonlyNames: readonly string[] = ["a"];
const [] is never[]

const items = [] is inferred as never[], so every push is an error. Annotate it — const items: Door[] = [] — or give it a first element.

Literal types and unions of them

A union of string literals replaces a comment, a constant file and a runtime check. It is the first thing to reach for whenever a value is "one of these".

the most useful small feature in the language
type Finish = "painted" | "oiled" | "glazed";

function order(finish: Finish) { … }

order("painted");    // fine
order("gold");       // Argument of type '"gold"' is not assignable to 'Finish'

// and the editor offers the three — which is half the value

any: the off switch

everything is allowed, and nothing is checked
let value: any = "Painted";
value.nonsense();        // no error
value.a.b.c;             // no error
const n: number = value; // no error — and wrong
any spreads

Anything derived from an any is also any. One of them in a shared helper quietly turns off checking across half a codebase — which is why noImplicitAny exists and why as any is a decision, not a shortcut.

unknown: the honest one

unknown is what any should have been. Use it for anything arriving from outside — a parsed JSON, a caught error, a message from a worker — and narrow it before you touch it.

you must prove what it is before using it
let value: unknown = JSON.parse(text);

value.name;                       // Error: 'value' is of type 'unknown'

if (typeof value === "string") {
  value.toUpperCase();            // now it is a string
}

if (value && typeof value === "object" && "name" in value) {
  …
}

never, and why it appears

a value that cannot exist
function fail(message: string): never {   // it never returns
  throw new Error(message);
}

const impossible: string & number = …;    // never

// the useful one: proving a switch is exhaustive
function label(finish: Finish): string {
  switch (finish) {
    case "painted": return "Painted";
    case "oiled": return "Oiled";
    case "glazed": return "Glazed";
    default: {
      const exhaustive: never = finish;   // an error here means a case is missing
      return exhaustive;
    }
  }
}

Assertions, and when they are honest

as silences the compiler; it changes nothing at runtime. It is reasonable when you genuinely know something the checker cannot — the shape of your own HTML — and a lie when the value came from outside.

"trust me" — so be worth trusting
const el = document.querySelector("#q") as HTMLInputElement;   // you checked the markup
const door = JSON.parse(text) as Door;                         // you did NOT check anything

const x = "5" as unknown as number;   // the double assertion: always a smell

// satisfies checks the shape without widening the type
const config = { url: "/api", retries: 3 } satisfies Config;

Practice