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.
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 name = "Painted" has the literal type "Painted", because it can never be anything else. let widens to string, because it can.
The basic types
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
const names: string[] = [];
const names: Array<string> = [];
const doors: Door[] = [];
const grid: number[][] = [];
const mixed: (string | number)[] = [];
const readonlyNames: readonly string[] = ["a"];
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".
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
let value: any = "Painted";
value.nonsense(); // no error
value.a.b.c; // no error
const n: number = value; // no error — and wrong
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.
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
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.
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;