Web development cheat sheetTypeScript logo

TypeScript cheat sheet

TypeScript on one page: types, interfaces, unions and narrowing, generics, utility types, enums, satisfies and tsconfig, checked with TypeScript 7.

Last updated

TypeScript is JavaScript with types: you write what each value is allowed to be, and the compiler catches the mismatch before the code runs. The reference below is grouped by what you are trying to do, and the filter box searches all of it at once. Type generic and everything about generics comes to you, or type tsconfig for the compiler options worth setting.

Every snippet is checked against TypeScript 7.0, type-checked with tsc under strict, and the worked examples were run on Node.js 24. Names like user, shape and items are placeholders for your own. Everything that happens at runtime is plain JavaScript, so keep the JavaScript cheat sheet open beside this one for array methods, promises and the DOM.

Running TypeScript

TaskCodeNotes
Add TypeScript to a projectnpm install --save-dev typescriptPins the version per project. npx tsc runs that copy
Check which version you havenpx tsc --versionThis page is checked against 7.0
Create a tsconfig.jsonnpx tsc --initStrict, with the options that matter already switched on
Type-check without writing filesnpx tsc --noEmitWhat most projects run in CI when a bundler does the building
Compile to JavaScriptnpx tscReads tsconfig.json and writes .js files to outDir
Check again on every savenpx tsc --noEmit --watch
Use a tsconfig somewhere elsenpx tsc -p packages/api
See the settings after defaults and extendsnpx tsc --showConfig
Run a .ts file directlynode app.tsNode.js 22.18 and later strip the types without checking them. No enums or namespaces
Add the types for Node.jsnpm install --save-dev @types/nodeThen list "node" in types in tsconfig.json. TypeScript 7 loads no @types package you have not listed
Check a JavaScript file// @ts-checkFirst line of a .js file. JSDoc comments act as the types
Expect an error on the next line// @ts-expect-errorItself an error when there is none, so it cannot go stale. Better than @ts-ignore

Basic and literal types

TypeScript is JavaScript with types written after a colon. The types are checked by tsc and then deleted, so none of this exists when the code runs.

TaskCodeNotes
Give a variable a typelet count: number = 0;Usually not needed: let count = 0 is inferred as number
The primitive typesstring number boolean bigint symbol null undefinedLower case. String with a capital S is the wrapper object type
Arrayconst ids: number[] = [1, 2, 3];Array<number> is the same type
Read-only arrayconst days: readonly string[] = ["Mon", "Tue"];No push, no sort in place. Checked at compile time only
Tuple: fixed length, a type per positionconst point: [number, number] = [3, 4];
Tuple with names and an optional itemtype Range = [start: number, end?: number];The names are for readers and editor hints only
Object typeconst user: { name: string; age?: number } = { name: "Ada" };age? means the property can be missing
Literal typelet direction: "up" | "down" = "up";Only those exact strings
const keeps the literalconst mode = "dark";Type "dark". With let it would be string
Freeze literal typesconst sizes = ["S", "M", "L"] as const;Type readonly ["S", "M", "L"] instead of string[]
Union from an as const arraytype Size = (typeof sizes)[number];"S" | "M" | "L". One list for the values and the type
Something or nothinglet nickname: string | null = null;With strict on, null is not a valid string
Any value, checked before uselet input: unknown = JSON.parse(text);You must narrow it first. The safe type for data from outside
Switch checking offlet legacy: any;Anything goes, and it spreads to everything it touches
Returns nothingfunction log(message: string): void { console.log(message); }
Never returnsfunction fail(message: string): never { throw new Error(message); }Also the type of a case that cannot happen
Object with any string keysconst scores: Record<string, number> = {};Same as { [name: string]: number }
Pattern of stringstype CssSize = `${number}px`;A template literal type. "12px" fits, "12em" does not
The type of an existing valuetype Config = typeof config;typeof in a type position reads the type, not the runtime string

Let inference do most of the work. Annotate function parameters, return types that other files rely on, and empty values such as [] or null that TypeScript cannot guess the future contents of.

Interfaces vs types

TaskCodeNotes
Interfaceinterface User { id: number; name: string; }
Type alias for the same shapetype User = { id: number; name: string };Interchangeable with the interface for plain object shapes
Optional propertyemail?: string;
Read-only propertyreadonly id: number;Cannot be assigned after the object is made. Shallow
Methodgreet(greeting: string): string;
Extend an interfaceinterface Admin extends User { permissions: string[]; }
Combine type aliasestype Admin = User & { permissions: string[] };An intersection: every property of both
Add to an existing interfaceinterface User { avatarUrl?: string; }Declaration merging: a second interface with the same name adds to the first. A type alias cannot
Union, only with typetype Id = number | string;Unions, tuples, primitives and mapped types need a type alias
Any number of keysinterface Scores { [player: string]: number; }An index signature
Function typetype Handler = (event: Event) => void;
Class that follows an interfaceclass Member implements User { id = 1; name = "Ada"; }Checks the class. Adds nothing to it

Both work for object shapes. Interfaces give clearer error messages and can be extended or merged; type aliases can also name unions, tuples and computed types. A common rule: interface for object shapes, type for everything else. Consistency matters more than the choice.

Unions and narrowing

A union is a value that can be one of several types. Narrowing is how TypeScript follows your if statements to work out which one it is at each line.

TaskCodeNotes
Unionlet id: number | string;Only what both types share can be used until it is narrowed
Narrow with typeofif (typeof id === "string") { id.toUpperCase(); }Inside the block, id is string
Narrow out null and undefinedif (user) { user.name; }Truthiness also rules out 0 and the empty string
Bail out earlyif (!user) return;Below this line, user is not null or undefined
Narrow with inif ("swim" in animal) { animal.swim(); }
Narrow with instanceofif (err instanceof Error) { console.error(err.message); }catch variables are unknown under strict, so this is the usual first step
Narrow an arrayif (Array.isArray(input)) { input.length; }
Discriminated uniontype Shape = { kind: "circle"; r: number } | { kind: "square"; size: number };Every member has kind, with a different literal
Narrow by the shared tagif (shape.kind === "circle") { shape.r; }switch (shape.kind) works the same way
Make sure every case is handledconst unhandled: never = shape;In the default branch. Fails to compile when a new kind is added and missed
Tell TypeScript it is not nullconst app = document.getElementById("app")!;No runtime check. A wrong guess crashes later instead
Drop undefined from an arrayconst names = list.filter((n) => n !== undefined);TS 5.5 infers the check, so the result is string[]

Functions

TaskCodeNotes
Parameter and return typesfunction add(a: number, b: number): number { return a + b; }The return type can be inferred. Writing it catches a wrong return
Arrow functionconst add = (a: number, b: number): number => a + b;
Optional parameterfunction greet(name?: string) {}string | undefined inside the function
Default valuefunction greet(name = "world") {}Type inferred from the default
Any number of argumentsfunction sum(...nums: number[]) { return nums.reduce((a, b) => a + b, 0); }
Named optionsfunction connect({ host, port = 5432 }: { host: string; port?: number }) {}The type goes after the whole pattern, not inside it
Variable that holds a functionlet onSave: (id: number) => void;
Callback parameterfunction retry(task: () => Promise<void>, times = 3) {}
Async functionasync function loadName(id: number): Promise<string> { return `user ${id}`; }The return type of an async function is always a Promise
Overloadsfunction parse(x: string): number; function parse(x: number): string;Signatures first, then one implementation that handles every case
Type of thisfunction onClick(this: HTMLButtonElement) { this.disabled = true; }Not a real parameter. Removed from the JavaScript

Generics

A generic is a type parameter: a placeholder, usually T, that is filled in each time the function or type is used, so the output type can follow the input type.

TaskCodeNotes
Generic functionfunction first<T>(items: T[]): T | undefined { return items[0]; }
Call it and let T be inferredfirst(["a", "b"])T is string, so the result is string | undefined
Pass the type yourselffirst<number>([])Needed when there is nothing to infer from
Generic typetype ApiResponse<T> = { data: T; error?: string };
Generic interfaceinterface Box<T> { value: T; }
Generic classclass Stack<T> { #items: T[] = []; push(item: T) { this.#items.push(item); } }
Limit what T can befunction longest<T extends { length: number }>(a: T, b: T) { return a.length >= b.length ? a : b; }Works for strings, arrays, anything with a length
A key of the objectfunction get<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }get(user, "name") is a string. get(user, "nmae") does not compile
Default type parametertype Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
Keep literal types when inferringfunction tuple<const T extends readonly unknown[]>(...args: T) { return args; }TS 5.0. tuple("a", 1) is readonly ["a", 1], not (string | number)[]
Stop an argument driving inferencefunction pick<T extends string>(options: T[], fallback: NoInfer<T>) {}TS 5.4. The fallback must be one of the options
Generic arrow function in a .tsx fileconst identity = <T,>(value: T) => value;The comma stops <T> being read as JSX

keyof, typeof and mapped types

TaskCodeNotes
Union of an object's keystype UserKey = keyof User;"id" | "name" for the User above
Type of one propertytype UserName = User["name"];An indexed access type
Type of a valuetype Theme = typeof theme;
Union of an object's valuestype Colour = (typeof colours)[keyof typeof colours];With colours declared as const
Type of an array's itemstype Item = (typeof items)[number];
Transform every propertytype Flags<T> = { [K in keyof T]: boolean };A mapped type. Partial and Readonly are built this way
Rename every keytype Getters<T> = { [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K] };Key remapping with as
Choose a type by a conditiontype IsString<T> = T extends string ? true : false;A conditional type
Pull a type out of anothertype ItemOf<T> = T extends (infer U)[] ? U : never;infer names the part that matched
Combine string literal typestype HandlerName = `on${Capitalize<"click" | "focus">}`;"onClick" | "onFocus"

Utility types

Built-in generic types that make a new type from an existing one. Nothing to import. The examples use interface User { id: number; name: string; email?: string }.

TaskCodeNotes
Every property optionaltype UserPatch = Partial<User>;For update functions that take a few fields
Every property requiredtype FullUser = Required<User>;Removes every ?
Every property read-onlytype FrozenUser = Readonly<User>;Shallow, and compile time only. Object.freeze for runtime
Keep some propertiestype UserPreview = Pick<User, "id" | "name">;
Drop some propertiestype NewUser = Omit<User, "id">;Omit does not check the key exists, so a typo drops nothing
Object with known keys and one value typetype Permissions = Record<"admin" | "member", string[]>;Every key is required
Remove members from a uniontype Open = Exclude<"draft" | "open" | "closed", "closed">;"draft" | "open"
Keep members of a uniontype Clicks = Extract<AppEvent, { type: "click" }>;Works on discriminated unions
Remove null and undefinedtype Name = NonNullable<string | null | undefined>;string
What a function returnstype Created = ReturnType<typeof createUser>;
A function's parameterstype CreateArgs = Parameters<typeof createUser>;A tuple. CreateArgs[0] is the first
What a promise resolves totype Loaded = Awaited<ReturnType<typeof loadUser>>;Unwraps nested promises too
The object a class makestype CatInstance = InstanceType<typeof Cat>;Usually just write Cat
Change the case of a string typetype Loud = Uppercase<"hi">;Also Lowercase, Capitalize, Uncapitalize

Enums

Enums are one of the few TypeScript features that produce JavaScript: an object that exists at runtime. That is why Node.js cannot run them by stripping types, and why many teams use a union of literals instead.

TaskCodeNotes
Numeric enumenum Direction { Up, Down, Left, Right }Up is 0, Down is 1 and so on
String enumenum Status { Active = "active", Archived = "archived" }Easier to read in logs and JSON
Use a memberconst s: Status = Status.Active;
Name from a numberDirection[0]Up. Numeric enums only
Loop over a string enumObject.values(Status)active and archived. A numeric enum gives names and numbers mixed
Inlined enumconst enum Flag { Read = 1, Write = 2 }Replaced by the numbers at compile time. Avoid in libraries
The usual alternative: a uniontype Role = "admin" | "member";No runtime code at all
Alternative with runtime valuesconst Role = { Admin: "admin", Member: "member" } as const;An ordinary object you can loop over
Its type, with the same nametype Role = (typeof Role)[keyof typeof Role];"admin" | "member". A value and a type can share a name
Forbid enums and other non-erasable syntax"erasableSyntaxOnly": trueIn tsconfig. Also rejects namespaces and parameter properties

Type guards

A type guard is a check TypeScript understands. typeof, instanceof, in and === are built in. When the check is more involved, write a function that returns a type predicate.

TaskCodeNotes
Your own type guardfunction isString(x: unknown): x is string { return typeof x === "string"; }x is string is the type predicate
Use itif (isString(value)) { value.toUpperCase(); }
Guard for one member of a unionfunction isCircle(s: Shape): s is Circle { return s.kind === "circle"; }
Filter with a guardconst users = rows.filter((row): row is User => row !== null);User[] rather than (User | null)[]
Check the shape of unknown dataif (typeof data === "object" && data !== null && "id" in data) { data.id; }data.id is unknown here, but reading it compiles
Throw instead of returning falsefunction assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }After assert(user, ...) the rest of the code knows user is set
Assert a specific typefunction assertIsUser(x: unknown): asserts x is User { if (!isUser(x)) throw new TypeError("not a user"); }
Keys of an object, typed(Object.keys(user) as (keyof User)[])Object.keys returns string[] because objects can carry extra keys

TypeScript trusts a type predicate completely. If isUser returns true for something that is not a User, every line after it is wrong with no warning, so keep guards small and test them.

satisfies, as and as const

TaskCodeNotes
Check a value against a type, keep its own typeconst palette = { primary: "#f5c518", spacing: 8 } satisfies Record<string, string | number>;TS 4.9. palette.spacing is still number
The same with an annotationconst palette: Record<string, string | number> = { primary: "#f5c518", spacing: 8 };palette.spacing is string | number, and palette.typo compiles
Literal types, checkedconst routes = { home: "/", blog: "/blog" } as const satisfies Record<string, `/${string}`>;Keeps the exact strings and checks each starts with /
Catch typos in a config objectexport default { port: 3000, host: "localhost" } satisfies ServerConfig;An unknown key or a wrong type is an error
Type assertionconst email = document.querySelector("#email") as HTMLInputElement;No runtime check. You are telling, not asking
The same, with a type argumentdocument.querySelector<HTMLInputElement>("#email")Keeps the null in the result, which is more honest
Force an unrelated typeconst legacy = value as unknown as LegacyUser;A last resort, and a sign the types need fixing
Not null or undefinedconst cached = cache.get(key)!;The non-null assertion. Same risk as as

Prefer satisfies to an annotation for constants, and both to as. An annotation throws away what TypeScript knew about the value, and as switches the check off.

Classes

TaskCodeNotes
Typed fieldsclass Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } }Fields must be set in the constructor under strict
Declare and assign in one goconstructor(private readonly db: Database) {}A parameter property. Not erasable, so Node.js cannot strip it
Visibilitypublic protected privateCompile-time only. #field is private at runtime too
Read-only fieldreadonly createdAt = new Date();
Follow an interfaceclass ConsoleLogger implements Logger { log(message: string) { console.log(message); } }
Abstract classabstract class Shape { abstract area(): number; }Cannot be created with new. Subclasses must write area
Mark an overrideoverride speak() { return "meow"; }With noImplicitOverride, required, which catches a misspelt method
Field set somewhere TypeScript cannot seename!: string;Definite assignment. Use sparingly
Static memberstatic count = 0;

Modules and declarations

TaskCodeNotes
Export a typeexport interface User { id: number; name: string; }export type works the same way
Import only a typeimport type { User } from "./types.js";Always removed from the JavaScript. Required for types under verbatimModuleSyntax
Import a type and a value togetherimport { type User, createUser } from "./users.js";
Import with the .ts extensionimport { add } from "./math.ts";For Node.js running .ts directly. Needs allowImportingTsExtensions or rewriteRelativeImportExtensions
Import a JSON fileimport config from "./config.json" with { type: "json" };Needs resolveJsonModule. The type comes from the file
Types for a package that has nonenpm install --save-dev @types/lodashMany packages ship their own types and need nothing
A global from a script tagdeclare const API_URL: string;declare says it exists. It emits nothing
Add to a global typedeclare global { interface Window { analytics: Analytics } }Inside a module file
A module with no types at alldeclare module "legacy-widget";In a .d.ts file. Everything from it is any

tsconfig options that matter

tsconfig.json sits at the project root and sets how tsc checks and compiles. These are the compilerOptions worth knowing. TypeScript 7 changed several defaults and removed a handful of old options, noted below.

OptionSettingWhat it does
strict"strict": trueOn by default in TypeScript 7. Turns on strictNullChecks, noImplicitAny and the rest. Leave it on
target"target": "es2025"Which JavaScript version to output. es2025 is the default. es5 was removed
module"module": "nodenext"For code Node.js runs. Use preserve when a bundler such as Vite builds it
moduleResolution"moduleResolution": "bundler"Goes with module preserve. The old node value was removed
lib"lib": ["es2025", "dom"]Which built-in APIs exist. Add dom for browser code, leave it out for Node.js
types"types": ["node"]Which @types packages load. Empty unless you list them
noEmit"noEmit": trueCheck only. For when a bundler or Node.js runs the code
outDir and rootDir"outDir": "dist", "rootDir": "src"Where the .js goes, and the folder its layout mirrors
noUncheckedIndexedAccess"noUncheckedIndexedAccess": trueitems[0] is T | undefined. Catches reads past the end. Not part of strict
exactOptionalPropertyTypes"exactOptionalPropertyTypes": trueage?: number no longer accepts an explicit undefined. Not part of strict
verbatimModuleSyntax"verbatimModuleSyntax": trueImports are kept as written, so type-only imports must say import type
isolatedModules"isolatedModules": trueErrors on code a one-file-at-a-time compiler like esbuild cannot handle
erasableSyntaxOnly"erasableSyntaxOnly": trueOnly syntax that can be deleted. For code Node.js runs by stripping types
noImplicitOverride"noImplicitOverride": trueA method that replaces a parent's method must say override
skipLibCheck"skipLibCheck": trueSkips checking .d.ts files. Faster, and quiet about clashes between packages
paths"paths": { "@/*": ["./src/*"] }Import aliases. Your bundler must know them too. baseUrl was removed
declaration"declaration": trueWrites .d.ts files. For publishing a library
sourceMap"sourceMap": trueStack traces and debuggers point at the .ts line
jsx"jsx": "react-jsx"For React. .tsx files only
allowJs and checkJs"allowJs": true, "checkJs": trueCompile, then also check, .js files. For moving a project over gradually
Share settings"extends": "@tsconfig/node24/tsconfig.json"Top level, beside compilerOptions. Community bases for each runtime

Narrowing a discriminated union, start to finish

Most of the unions table in one place: a union with a shared kind tag, a switch that narrows on it, and a never check that turns a forgotten case into a compile error.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2; // shape is the circle here
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default: {
      const unhandled: never = shape;
      throw new Error(`Unknown shape: ${JSON.stringify(unhandled)}`);
    }
  }
}
 
const shapes: Shape[] = [
  { kind: "circle", radius: 1 },
  { kind: "rectangle", width: 3, height: 4 },
  { kind: "triangle", base: 6, height: 2 },
];
 
for (const shape of shapes) {
  console.log(shape.kind, area(shape).toFixed(2));
}
// circle 3.14
// rectangle 12.00
// triangle 6.00

Add { kind: "square"; size: number } to Shape and the file stops compiling at const unhandled: never = shape, because a square can reach the default branch and a square is not never. That error is the point: the compiler finds every switch that needs a new case. Inside each case, reading shape.radius on the rectangle is an error too, because TypeScript knows which member it has.

Generics and utility types, start to finish

A small typed store. One generic class, with Omit and Partial deriving the input types from the record type so there is only one definition to keep up to date.

interface Entity {
  id: number;
}
 
class Store<T extends Entity> {
  #items = new Map<number, T>();
  #nextId = 1;
 
  add(data: Omit<T, "id">): T {
    const item = { ...data, id: this.#nextId++ } as T;
    this.#items.set(item.id, item);
    return item;
  }
 
  update(id: number, changes: Partial<Omit<T, "id">>): T {
    const current = this.#items.get(id);
    if (!current) {
      throw new Error(`No item with id ${id}`);
    }
    const updated = { ...current, ...changes };
    this.#items.set(id, updated);
    return updated;
  }
 
  find<K extends keyof T>(key: K, value: T[K]): T[] {
    return [...this.#items.values()].filter((item) => item[key] === value);
  }
}
 
interface Task extends Entity {
  title: string;
  done: boolean;
  priority: "low" | "high";
}
 
const tasks = new Store<Task>();
tasks.add({ title: "Write the cheat sheet", done: false, priority: "high" });
tasks.add({ title: "Feed the cat", done: false, priority: "high" });
tasks.update(2, { done: true });
 
console.log(tasks.find("priority", "high").length); // 2
console.log(tasks.find("done", true).map((t) => t.title)); // [ 'Feed the cat' ]
 
// @ts-expect-error: "urgent" is not "low" | "high"
tasks.find("priority", "urgent");
// @ts-expect-error: id is left out of the input type
tasks.add({ id: 99, title: "Sneaky", done: false, priority: "low" });

K extends keyof T is what ties the two arguments of find together: once the key is "priority", the value has to be T["priority"], which is "low" | "high". The one as T is honest about a limit of generics: TypeScript cannot prove that spreading an Omit<T, "id"> and adding an id gives back exactly a T, even though it does.

satisfies, an annotation and as, side by side

Three ways to type the same object, and what each one lets through. Each @ts-expect-error line is a mistake the compiler catches; remove the comment and it fails to compile.

type Colour = `#${string}`;
 
// 1. An annotation: checked, but the specific keys are forgotten
const annotated: Record<string, Colour> = { primary: "#f5c518", text: "#111111" };
annotated.primry; // a typo, and no error: every string key is allowed
 
// 2. satisfies: checked, and the keys are kept
const theme = { primary: "#f5c518", text: "#111111" } satisfies Record<string, Colour>;
// @ts-expect-error: Property 'primry' does not exist
theme.primry;
const primary: Colour = theme.primary; // fine: the value type is still checked
 
// @ts-expect-error: "red" does not match `#${string}`
const broken = { primary: "red" } satisfies Record<string, Colour>;
 
// 3. as: no check at all
const guessed = { primary: "red" } as unknown as Record<string, Colour>; // compiles, and is wrong
 
// as const satisfies: exact values, still checked
const sizes = { sm: 640, md: 768, lg: 1024 } as const satisfies Record<string, number>;
type Breakpoint = keyof typeof sizes; // "sm" | "md" | "lg"
const widest: 1024 = sizes.lg;
 
console.log(primary, widest, Object.keys(annotated).length, broken, guessed);

Reach for satisfies for config objects and lookup tables, where you want the check and the precise type. An annotation is right when the value really should be treated as the wider type, such as a variable that will be reassigned. as is for the rare case where you know something the compiler cannot, and it is where bugs hide.

Type guards for data from an API, start to finish

Types disappear at runtime, so JSON from a server is unknown until something checks it. A type guard does the checking once, at the edge, and everything after it can trust the type.

interface User {
  id: number;
  name: string;
  email?: string;
}
 
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "number" &&
    "name" in value &&
    typeof value.name === "string" &&
    (!("email" in value) || typeof value.email === "string")
  );
}
 
function parseUsers(json: string): User[] {
  const data: unknown = JSON.parse(json);
  if (!Array.isArray(data)) {
    throw new TypeError("Expected an array of users");
  }
  return data.filter(isUser);
}
 
const body = '[{"id":1,"name":"Ada"},{"id":"2","name":"Grace"},{"id":3,"name":"Linus","email":"l@example.com"}]';
 
const users = parseUsers(body);
console.log(users.map((u) => u.name)); // [ 'Ada', 'Linus' ]: Grace's id is a string
 
try {
  parseUsers('{"id":1}');
} catch (err) {
  if (err instanceof Error) {
    console.log(err.message); // Expected an array of users
  }
}

data.filter(isUser) returns User[], not any[], because isUser is a type predicate. JSON.parse returns any, and assigning it to unknown straight away is what forces the check: with any, data.map((u) => u.name) would compile and fail at runtime on the first malformed item. For bigger shapes, a validation library such as Zod writes the guard and the type from one schema. To look at a real response before you type it, paste it into the JSON formatter.

A tsconfig.json for a new project

A starting point for code that Node.js runs, with the checks from the table above that are worth having from day one. tsc --init writes something close to it.

{
  "compilerOptions": {
    // Where the code runs
    "target": "es2025",
    "module": "nodenext",
    "lib": ["es2025"],
    "types": ["node"],
 
    // Output
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
 
    // Checks beyond strict, which is on by default
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
 
    // Keep each file compilable on its own
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

For a browser app built by Vite or another bundler, change module to "preserve", add "moduleResolution": "bundler" and "noEmit": true, and put "dom" in lib. For code Node.js runs directly as .ts, add "erasableSyntaxOnly": true and "noEmit": true and import files with their .ts extension, which "rewriteRelativeImportExtensions": true turns into .js if you later compile with tsc.

Gotchas

The mistakes almost everyone makes in their first month of TypeScript.

Looks rightWhat actually happensDo this instead
const user = res.json() as UserCompiles whatever the server sentCheck it with a type guard or a schema
function f(x: any)Every mistake with x goes unchecked, and any spreadsunknown, then narrow
items[0].name on an empty arrayCompiles, then a TypeError at runtimenoUncheckedIndexedAccess, or items[0]?.name
catch (err) { err.message }Error: err is unknown under strictif (err instanceof Error) first
Object.keys(user).forEach((k) => user[k])Error: k is string, not keyof Userfor (const [k, v] of Object.entries(user))
const colours: Record<string, string> = { ... }colours.typo compiles: the keys are forgottensatisfies Record<string, string>
Omit<User, "emial">Compiles, and omits nothingCheck the key, or Pick the ones you keep
interface User declared twiceThe two merge silentlyRename one, or use a type
enum run with node app.tsSyntaxError: Node.js only strips typesA union, or an object as const
document.getElementById("app")!Crashes later if the id is wrongif (!app) throw new Error(...)
"moduleResolution": "node"Error in TypeScript 7: the option was removed"bundler" or "nodenext"
"baseUrl": "."Error in TypeScript 7: the option was removed"paths" with ./ in front of each target
import { User } from "./types.js"With verbatimModuleSyntax, an error for a type-only importimport type { User }
if (count) to rule out undefinedAlso skips 0if (count !== undefined)

Common questions

Which version of TypeScript does this cheat sheet cover?

TypeScript 7.0, the current release. Every snippet was type-checked with tsc 7.0.2 under strict, and the worked examples were run on Node.js 24. TypeScript 7 is the native compiler written in Go. The language is the one you know from 5.x, but strict is now on by default and it has removed old options such as target es5, moduleResolution node, baseUrl and outFile, so an older tsconfig may need tidying before it upgrades.

Should I use interface or type in TypeScript?

For an object shape, either works and the difference rarely matters. Interfaces can be extended with extends and merged by declaring the same name twice, and tend to give clearer error messages. Type aliases can do everything else: unions, tuples, primitives, mapped and conditional types. A common rule is interface for object shapes and type for the rest. Whichever you pick, be consistent across the codebase.

What is the difference between any and unknown?

Both accept any value. any also switches off checking, so you can call anything on it and the mistake only shows up when the code runs, and it spreads: a value read from an any is any too. unknown makes you prove what the value is first, with typeof, instanceof or a type guard, before you can use it. Use unknown for JSON, caught errors and anything else from outside your code.

Does TypeScript check types when the code runs?

No. The types are checked by tsc and then removed, so the JavaScript that runs has no idea what a User is. Data from an API, a form or JSON.parse is whatever it is, whatever the type annotation says. Check it at the boundary with a type guard or a validation library such as Zod, and from then on the types can be trusted.

Should I use an enum or a union of strings?

A union of string literals, such as type Role = "admin" | "member", is the common choice in new code. It produces no JavaScript, works when Node.js runs TypeScript directly, and reads the same in JSON and logs. Enums generate a runtime object and cannot be stripped. If you need the list at runtime too, use an object with as const and derive the type from it.

What does satisfies do in TypeScript?

satisfies, added in TypeScript 4.9, checks that a value matches a type without changing the type TypeScript infers for it. With an annotation such as const theme: Record<string, string>, TypeScript forgets which keys the object has. With satisfies Record<string, string>, it still checks every value is a string, but theme.primary stays a known key and a typo is still an error. It is the right tool for config objects and lookup tables.

How do I run a TypeScript file?

On Node.js 22.18 or later, node app.ts runs it directly: Node removes the types and runs what is left, without checking them. That only works for erasable syntax, so not enums, namespaces or parameter properties. Run npx tsc --noEmit separately to check the types. Older projects compile with tsc first and run the .js, or use a runner such as tsx.

Should I learn JavaScript before TypeScript?

Yes, at least the basics. TypeScript is JavaScript with types added, so every runtime behaviour, from how this works to what an array method returns, is JavaScript's. Once you are comfortable with variables, functions, arrays, objects and promises in JavaScript, adding types takes days rather than weeks, and most of the type errors you meet will point at real bugs.

What does strict mode turn on in TypeScript?

strict is a shortcut for a group of checks: strictNullChecks, so null and undefined are not valid values of every type; noImplicitAny, so a parameter with no type is an error rather than any; useUnknownInCatchVariables, so caught errors are unknown; plus strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, strictBuiltinIteratorReturn and noImplicitThis. It is on by default in TypeScript 7. noUncheckedIndexedAccess is not part of it and is worth adding.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.