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.
Searches the task, the command and the third column. Press / from anywhere on the page.
167 commands
Running TypeScript
| Task | Code | Notes |
|---|---|---|
| Add TypeScript to a project | npm install --save-dev typescript | Pins the version per project. npx tsc runs that copy |
| Check which version you have | npx tsc --version | This page is checked against 7.0 |
| Create a tsconfig.json | npx tsc --init | Strict, with the options that matter already switched on |
| Type-check without writing files | npx tsc --noEmit | What most projects run in CI when a bundler does the building |
| Compile to JavaScript | npx tsc | Reads tsconfig.json and writes .js files to outDir |
| Check again on every save | npx tsc --noEmit --watch | |
| Use a tsconfig somewhere else | npx tsc -p packages/api | |
| See the settings after defaults and extends | npx tsc --showConfig | |
| Run a .ts file directly | node app.ts | Node.js 22.18 and later strip the types without checking them. No enums or namespaces |
| Add the types for Node.js | npm install --save-dev @types/node | Then list "node" in types in tsconfig.json. TypeScript 7 loads no @types package you have not listed |
| Check a JavaScript file | // @ts-check | First line of a .js file. JSDoc comments act as the types |
| Expect an error on the next line | // @ts-expect-error | Itself 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.
| Task | Code | Notes |
|---|---|---|
| Give a variable a type | let count: number = 0; | Usually not needed: let count = 0 is inferred as number |
| The primitive types | string number boolean bigint symbol null undefined | Lower case. String with a capital S is the wrapper object type |
| Array | const ids: number[] = [1, 2, 3]; | Array<number> is the same type |
| Read-only array | const days: readonly string[] = ["Mon", "Tue"]; | No push, no sort in place. Checked at compile time only |
| Tuple: fixed length, a type per position | const point: [number, number] = [3, 4]; | |
| Tuple with names and an optional item | type Range = [start: number, end?: number]; | The names are for readers and editor hints only |
| Object type | const user: { name: string; age?: number } = { name: "Ada" }; | age? means the property can be missing |
| Literal type | let direction: "up" | "down" = "up"; | Only those exact strings |
| const keeps the literal | const mode = "dark"; | Type "dark". With let it would be string |
| Freeze literal types | const sizes = ["S", "M", "L"] as const; | Type readonly ["S", "M", "L"] instead of string[] |
| Union from an as const array | type Size = (typeof sizes)[number]; | "S" | "M" | "L". One list for the values and the type |
| Something or nothing | let nickname: string | null = null; | With strict on, null is not a valid string |
| Any value, checked before use | let input: unknown = JSON.parse(text); | You must narrow it first. The safe type for data from outside |
| Switch checking off | let legacy: any; | Anything goes, and it spreads to everything it touches |
| Returns nothing | function log(message: string): void { console.log(message); } | |
| Never returns | function fail(message: string): never { throw new Error(message); } | Also the type of a case that cannot happen |
| Object with any string keys | const scores: Record<string, number> = {}; | Same as { [name: string]: number } |
| Pattern of strings | type CssSize = `${number}px`; | A template literal type. "12px" fits, "12em" does not |
| The type of an existing value | type 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
| Task | Code | Notes |
|---|---|---|
| Interface | interface User { id: number; name: string; } | |
| Type alias for the same shape | type User = { id: number; name: string }; | Interchangeable with the interface for plain object shapes |
| Optional property | email?: string; | |
| Read-only property | readonly id: number; | Cannot be assigned after the object is made. Shallow |
| Method | greet(greeting: string): string; | |
| Extend an interface | interface Admin extends User { permissions: string[]; } | |
| Combine type aliases | type Admin = User & { permissions: string[] }; | An intersection: every property of both |
| Add to an existing interface | interface User { avatarUrl?: string; } | Declaration merging: a second interface with the same name adds to the first. A type alias cannot |
| Union, only with type | type Id = number | string; | Unions, tuples, primitives and mapped types need a type alias |
| Any number of keys | interface Scores { [player: string]: number; } | An index signature |
| Function type | type Handler = (event: Event) => void; | |
| Class that follows an interface | class 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.
| Task | Code | Notes |
|---|---|---|
| Union | let id: number | string; | Only what both types share can be used until it is narrowed |
| Narrow with typeof | if (typeof id === "string") { id.toUpperCase(); } | Inside the block, id is string |
| Narrow out null and undefined | if (user) { user.name; } | Truthiness also rules out 0 and the empty string |
| Bail out early | if (!user) return; | Below this line, user is not null or undefined |
| Narrow with in | if ("swim" in animal) { animal.swim(); } | |
| Narrow with instanceof | if (err instanceof Error) { console.error(err.message); } | catch variables are unknown under strict, so this is the usual first step |
| Narrow an array | if (Array.isArray(input)) { input.length; } | |
| Discriminated union | type Shape = { kind: "circle"; r: number } | { kind: "square"; size: number }; | Every member has kind, with a different literal |
| Narrow by the shared tag | if (shape.kind === "circle") { shape.r; } | switch (shape.kind) works the same way |
| Make sure every case is handled | const unhandled: never = shape; | In the default branch. Fails to compile when a new kind is added and missed |
| Tell TypeScript it is not null | const app = document.getElementById("app")!; | No runtime check. A wrong guess crashes later instead |
| Drop undefined from an array | const names = list.filter((n) => n !== undefined); | TS 5.5 infers the check, so the result is string[] |
Functions
| Task | Code | Notes |
|---|---|---|
| Parameter and return types | function add(a: number, b: number): number { return a + b; } | The return type can be inferred. Writing it catches a wrong return |
| Arrow function | const add = (a: number, b: number): number => a + b; | |
| Optional parameter | function greet(name?: string) {} | string | undefined inside the function |
| Default value | function greet(name = "world") {} | Type inferred from the default |
| Any number of arguments | function sum(...nums: number[]) { return nums.reduce((a, b) => a + b, 0); } | |
| Named options | function connect({ host, port = 5432 }: { host: string; port?: number }) {} | The type goes after the whole pattern, not inside it |
| Variable that holds a function | let onSave: (id: number) => void; | |
| Callback parameter | function retry(task: () => Promise<void>, times = 3) {} | |
| Async function | async function loadName(id: number): Promise<string> { return `user ${id}`; } | The return type of an async function is always a Promise |
| Overloads | function parse(x: string): number; function parse(x: number): string; | Signatures first, then one implementation that handles every case |
| Type of this | function 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.
| Task | Code | Notes |
|---|---|---|
| Generic function | function first<T>(items: T[]): T | undefined { return items[0]; } | |
| Call it and let T be inferred | first(["a", "b"]) | T is string, so the result is string | undefined |
| Pass the type yourself | first<number>([]) | Needed when there is nothing to infer from |
| Generic type | type ApiResponse<T> = { data: T; error?: string }; | |
| Generic interface | interface Box<T> { value: T; } | |
| Generic class | class Stack<T> { #items: T[] = []; push(item: T) { this.#items.push(item); } } | |
| Limit what T can be | function 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 object | function 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 parameter | type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }; | |
| Keep literal types when inferring | function 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 inference | function 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 file | const identity = <T,>(value: T) => value; | The comma stops <T> being read as JSX |
keyof, typeof and mapped types
| Task | Code | Notes |
|---|---|---|
| Union of an object's keys | type UserKey = keyof User; | "id" | "name" for the User above |
| Type of one property | type UserName = User["name"]; | An indexed access type |
| Type of a value | type Theme = typeof theme; | |
| Union of an object's values | type Colour = (typeof colours)[keyof typeof colours]; | With colours declared as const |
| Type of an array's items | type Item = (typeof items)[number]; | |
| Transform every property | type Flags<T> = { [K in keyof T]: boolean }; | A mapped type. Partial and Readonly are built this way |
| Rename every key | type Getters<T> = { [K in keyof T & string as `get${Capitalize<K>}`]: () => T[K] }; | Key remapping with as |
| Choose a type by a condition | type IsString<T> = T extends string ? true : false; | A conditional type |
| Pull a type out of another | type ItemOf<T> = T extends (infer U)[] ? U : never; | infer names the part that matched |
| Combine string literal types | type 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 }.
| Task | Code | Notes |
|---|---|---|
| Every property optional | type UserPatch = Partial<User>; | For update functions that take a few fields |
| Every property required | type FullUser = Required<User>; | Removes every ? |
| Every property read-only | type FrozenUser = Readonly<User>; | Shallow, and compile time only. Object.freeze for runtime |
| Keep some properties | type UserPreview = Pick<User, "id" | "name">; | |
| Drop some properties | type NewUser = Omit<User, "id">; | Omit does not check the key exists, so a typo drops nothing |
| Object with known keys and one value type | type Permissions = Record<"admin" | "member", string[]>; | Every key is required |
| Remove members from a union | type Open = Exclude<"draft" | "open" | "closed", "closed">; | "draft" | "open" |
| Keep members of a union | type Clicks = Extract<AppEvent, { type: "click" }>; | Works on discriminated unions |
| Remove null and undefined | type Name = NonNullable<string | null | undefined>; | string |
| What a function returns | type Created = ReturnType<typeof createUser>; | |
| A function's parameters | type CreateArgs = Parameters<typeof createUser>; | A tuple. CreateArgs[0] is the first |
| What a promise resolves to | type Loaded = Awaited<ReturnType<typeof loadUser>>; | Unwraps nested promises too |
| The object a class makes | type CatInstance = InstanceType<typeof Cat>; | Usually just write Cat |
| Change the case of a string type | type 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.
| Task | Code | Notes |
|---|---|---|
| Numeric enum | enum Direction { Up, Down, Left, Right } | Up is 0, Down is 1 and so on |
| String enum | enum Status { Active = "active", Archived = "archived" } | Easier to read in logs and JSON |
| Use a member | const s: Status = Status.Active; | |
| Name from a number | Direction[0] | Up. Numeric enums only |
| Loop over a string enum | Object.values(Status) | active and archived. A numeric enum gives names and numbers mixed |
| Inlined enum | const enum Flag { Read = 1, Write = 2 } | Replaced by the numbers at compile time. Avoid in libraries |
| The usual alternative: a union | type Role = "admin" | "member"; | No runtime code at all |
| Alternative with runtime values | const Role = { Admin: "admin", Member: "member" } as const; | An ordinary object you can loop over |
| Its type, with the same name | type 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": true | In 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.
| Task | Code | Notes |
|---|---|---|
| Your own type guard | function isString(x: unknown): x is string { return typeof x === "string"; } | x is string is the type predicate |
| Use it | if (isString(value)) { value.toUpperCase(); } | |
| Guard for one member of a union | function isCircle(s: Shape): s is Circle { return s.kind === "circle"; } | |
| Filter with a guard | const users = rows.filter((row): row is User => row !== null); | User[] rather than (User | null)[] |
| Check the shape of unknown data | if (typeof data === "object" && data !== null && "id" in data) { data.id; } | data.id is unknown here, but reading it compiles |
| Throw instead of returning false | function 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 type | function 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
| Task | Code | Notes |
|---|---|---|
| Check a value against a type, keep its own type | const palette = { primary: "#f5c518", spacing: 8 } satisfies Record<string, string | number>; | TS 4.9. palette.spacing is still number |
| The same with an annotation | const palette: Record<string, string | number> = { primary: "#f5c518", spacing: 8 }; | palette.spacing is string | number, and palette.typo compiles |
| Literal types, checked | const routes = { home: "/", blog: "/blog" } as const satisfies Record<string, `/${string}`>; | Keeps the exact strings and checks each starts with / |
| Catch typos in a config object | export default { port: 3000, host: "localhost" } satisfies ServerConfig; | An unknown key or a wrong type is an error |
| Type assertion | const email = document.querySelector("#email") as HTMLInputElement; | No runtime check. You are telling, not asking |
| The same, with a type argument | document.querySelector<HTMLInputElement>("#email") | Keeps the null in the result, which is more honest |
| Force an unrelated type | const legacy = value as unknown as LegacyUser; | A last resort, and a sign the types need fixing |
| Not null or undefined | const 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
| Task | Code | Notes |
|---|---|---|
| Typed fields | class 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 go | constructor(private readonly db: Database) {} | A parameter property. Not erasable, so Node.js cannot strip it |
| Visibility | public protected private | Compile-time only. #field is private at runtime too |
| Read-only field | readonly createdAt = new Date(); | |
| Follow an interface | class ConsoleLogger implements Logger { log(message: string) { console.log(message); } } | |
| Abstract class | abstract class Shape { abstract area(): number; } | Cannot be created with new. Subclasses must write area |
| Mark an override | override speak() { return "meow"; } | With noImplicitOverride, required, which catches a misspelt method |
| Field set somewhere TypeScript cannot see | name!: string; | Definite assignment. Use sparingly |
| Static member | static count = 0; |
Modules and declarations
| Task | Code | Notes |
|---|---|---|
| Export a type | export interface User { id: number; name: string; } | export type works the same way |
| Import only a type | import type { User } from "./types.js"; | Always removed from the JavaScript. Required for types under verbatimModuleSyntax |
| Import a type and a value together | import { type User, createUser } from "./users.js"; | |
| Import with the .ts extension | import { add } from "./math.ts"; | For Node.js running .ts directly. Needs allowImportingTsExtensions or rewriteRelativeImportExtensions |
| Import a JSON file | import config from "./config.json" with { type: "json" }; | Needs resolveJsonModule. The type comes from the file |
| Types for a package that has none | npm install --save-dev @types/lodash | Many packages ship their own types and need nothing |
| A global from a script tag | declare const API_URL: string; | declare says it exists. It emits nothing |
| Add to a global type | declare global { interface Window { analytics: Analytics } } | Inside a module file |
| A module with no types at all | declare 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.
| Option | Setting | What it does |
|---|---|---|
| strict | "strict": true | On 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": true | Check 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": true | items[0] is T | undefined. Catches reads past the end. Not part of strict |
| exactOptionalPropertyTypes | "exactOptionalPropertyTypes": true | age?: number no longer accepts an explicit undefined. Not part of strict |
| verbatimModuleSyntax | "verbatimModuleSyntax": true | Imports are kept as written, so type-only imports must say import type |
| isolatedModules | "isolatedModules": true | Errors on code a one-file-at-a-time compiler like esbuild cannot handle |
| erasableSyntaxOnly | "erasableSyntaxOnly": true | Only syntax that can be deleted. For code Node.js runs by stripping types |
| noImplicitOverride | "noImplicitOverride": true | A method that replaces a parent's method must say override |
| skipLibCheck | "skipLibCheck": true | Skips 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": true | Writes .d.ts files. For publishing a library |
| sourceMap | "sourceMap": true | Stack traces and debuggers point at the .ts line |
| jsx | "jsx": "react-jsx" | For React. .tsx files only |
| allowJs and checkJs | "allowJs": true, "checkJs": true | Compile, 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.00Add { 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 right | What actually happens | Do this instead |
|---|---|---|
const user = res.json() as User | Compiles whatever the server sent | Check it with a type guard or a schema |
function f(x: any) | Every mistake with x goes unchecked, and any spreads | unknown, then narrow |
items[0].name on an empty array | Compiles, then a TypeError at runtime | noUncheckedIndexedAccess, or items[0]?.name |
catch (err) { err.message } | Error: err is unknown under strict | if (err instanceof Error) first |
Object.keys(user).forEach((k) => user[k]) | Error: k is string, not keyof User | for (const [k, v] of Object.entries(user)) |
const colours: Record<string, string> = { ... } | colours.typo compiles: the keys are forgotten | satisfies Record<string, string> |
Omit<User, "emial"> | Compiles, and omits nothing | Check the key, or Pick the ones you keep |
interface User declared twice | The two merge silently | Rename one, or use a type |
enum run with node app.ts | SyntaxError: Node.js only strips types | A union, or an object as const |
document.getElementById("app")! | Crashes later if the id is wrong | if (!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 import | import type { User } |
if (count) to rule out undefined | Also skips 0 | if (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.
