TypeScript cheat sheet
A scannable TypeScript reference: 22 short snippets across 12 topics, each linking back to the lesson it came from.
At a glance
| Topic | What it covers | |
|---|---|---|
| Basic types and inference | TypeScript infers types from initial values, so most variables need no annotation. Annotate the boundaries - | lesson |
| Interfaces, type aliases and generics | TypeScript is structurally typed: a value matches a type when it has the required members, no matter where it came | lesson |
| tsconfig and tooling | Modern bundlers and runners such as esbuild, SWC, Vite and transpile-only ts-node strip types without verifying them | lesson |
| Narrowing, unions and exhaustive checks | A discriminated union is the most useful shape in TypeScript: one literal property that tells the compiler which | lesson |
| Typing functions, callbacks and this | Parameters, optional and rest arguments, overloads that model real call shapes, contextual typing for callbacks, and | lesson |
| Utility types, keyof and mapped types | Utility types are pure compile-time transforms: the emitted JavaScript is exactly what you would have written by hand | lesson |
| Classes, modifiers and abstract types | Access modifiers and parameter properties, runtime-private fields, implements versus extends, abstract members, and the | lesson |
| Generic patterns without over-engineering | Constraints and defaults, where type inference actually looks, generic data structures, and the judgement to stop | lesson |
| Typing asynchronous and external data | Promises and Awaited, typed fetch results, AbortSignal, and modelling failure as a value instead of hoping nothing | lesson |
| Runtime validation at the edges | Compilation removes every type, so the only thing that can refuse bad data at runtime is code you actually wrote. The | lesson |
| Declaration files and third-party types | Writing .d.ts declarations, using @types packages, typing an untyped dependency locally, and publishing types with your | lesson |
| Migrating a JavaScript project incrementally | The migration is a sequence of small steps, each of which leaves the build green. Start with allowJs so JavaScript | lesson |
Quick snippets
Basic types and inference
Primitives and inference
let title: string = 'Notes'; // explicit annotation
let count = 0; // inferred as number
let tags = ['a', 'b']; // inferred as string[]
let maybe: string | null = null; // union with null
const mode = 'dark'; // type is the literal "dark"
let loose = 'dark'; // type is string, because let can change
function add(a: number, b: number): number {
return a + b;
}
… 1 more lines in the full lesson.
Unions and narrowing
type Result =
| { ok: true; value: string }
| { ok: false; error: string };
function render(result: Result) {
if (result.ok) {
return result.value; // narrowed to the success branch
}
return result.error; // narrowed to the failure branch
}
// truthiness is not the same as presence… 4 more lines in the full lesson.
Full lesson: Basic types and inference →
Interfaces, type aliases and generics
Describing shapes
interface User {
id: number;
name: string;
email?: string; // optional member
readonly createdAt: Date; // cannot be reassigned after construction
}
interface Admin extends User {
permissions: string[];
}
type Point = { x: number; y: number }; // an object shape… 3 more lines in the full lesson.
Generics
function first<T>(items: T[]): T | undefined {
return items[0];
}
const number = first([1, 2, 3]); // number | undefined
// constraints keep the generic useful instead of powerless
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
pluck(users, 'id'); // number[]
// pluck(users, 'nope'); // error: not a key of User
… 7 more lines in the full lesson.
Full lesson: Interfaces, type aliases and generics →
tsconfig and tooling
Where the types actually get checked
# check only - writes no files
npx tsc --noEmit
# build, including .d.ts declaration files
npx tsc
# a bundler strips types without checking them
npx esbuild src/index.ts --bundle --outfile=dist/bundle.js
# so run the checker separately, in CI and in a pre-commit hook
npx tsc --noEmit && npm test
A tsconfig worth starting from
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"declaration": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,… 5 more lines in the full lesson.
Full lesson: tsconfig and tooling →
Narrowing, unions and exhaustive checks
Predicates, in and satisfies
// satisfies checks the value against a type but keeps the narrow inferred type
const config = {
retries: 3,
endpoint: '/api/v1'
} satisfies Record<string, string | number>;
config.retries.toFixed(0); // number, not string | number
// config.reteries; // error: the key does not exist
// an annotation would widen it instead
const widened: Record<string, string | number> = config;
// widened.retries.toFixed(0); // error: no such property on the wide type
Discriminated unions
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'rect'; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) { // the discriminant drives the narrowing
case 'circle':
return Math.PI * shape.radius ** 2; // only the circle payload exists here
case 'square':
return shape.side ** 2;
case 'rect':… 11 more lines in the full lesson.
Full lesson: Narrowing, unions and exhaustive checks →
Typing functions, callbacks and this
Signatures, optional and rest parameters
// a reusable function type: write the shape once, use it everywhere
type Mapper = (value: string, index: number) => number;
function repeat(value: string, times = 1, separator?: string): string {
return Array.from({ length: times }, () => value).join(separator ?? '');
}
// rest parameters collect the remainder into a real array
function sum(...values: number[]): number {
return values.reduce((total, value) => total + value, 0);
}
… 16 more lines in the full lesson.
The this parameter and detached methods
class Counter {
count = 0;
// a fake first parameter: erased at runtime, checked at compile time
increment(this: Counter, by: number): void {
this.count += by;
}
}
const counter = new Counter();
const detach = counter.increment;
// detach(1); // error: this would be undefined… 11 more lines in the full lesson.
Full lesson: Typing functions, callbacks and this →
Utility types, keyof and mapped types
The utility type toolkit
interface Article {
id: string;
title: string;
body: string;
publishedAt: Date;
}
type Draft = Partial<Article>; // every member optional
type Complete = Required<Draft>; // every member required again
type Card = Pick<Article, 'id' | 'title'>; // only these two members
type Meta = Omit<Article, 'body'>; // everything except body
type Headers = Record<string, string>; // any string key maps to string… 8 more lines in the full lesson.
keyof, indexed access and mapped types
// K constrained to a key of T ties the value to the field it writes
function set<T, K extends keyof T>(target: T, key: K, value: T[K]): T {
return { ...target, [key]: value };
}
set(article, 'title', 'Hello'); // ok
// set(article, 'title', 42); // error: number is not string
// set(article, 'titel', 'x'); // error: not a key of Article
// a mapped type rebuilds a shape member by member
type Nullable<T> = { [K in keyof T]: T[K] | null };
… 10 more lines in the full lesson.
Conditional and template literal types
// a conditional type picks a branch by assignability, with infer to capture a part
type ElementType<T> = T extends (infer U)[] ? U : T;
type Item = ElementType<string[]>; // string
type Self = ElementType<number>; // number
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Value = Unwrap<Promise<Promise<boolean>>>; // boolean
// template literal types build string unions out of other unions
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`; // 'onClick' | 'onFocus' | 'onBlur'
… 10 more lines in the full lesson.
Full lesson: Utility types, keyof and mapped types →
Classes, modifiers and abstract types
Modifiers and parameter properties
class Invoice {
// parameter properties declare and assign in one line
constructor(
public readonly id: string,
private items: number[] = [],
protected currency: string = 'USD'
) {}
#revision = 0; // a real runtime-private field
add(amount: number): void {
this.items.push(amount);… 16 more lines in the full lesson.
Full lesson: Classes, modifiers and abstract types →
Generic patterns without over-engineering
Where inference actually looks
// inference reads the arguments, so callers usually pass no type arguments
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const point = pair(1, 'x'); // [number, string]
// a type parameter that appears only in the return position has nothing to read
function empty<T>(): T[] { return []; }
// const bad = empty(); // unknown[]
const good = empty<string>(); // string[]
// a callback parameter is inferred contextually from the other argument… 16 more lines in the full lesson.
Knowing when to stop
// T appears once, so it links nothing and adds only noise
function logValue<T>(value: T): void {
console.log(value); // unknown would do exactly the same job
}
// two parameters with a relationship are worth one type parameter
function firstOr<T>(items: T[], fallback: T): T {
return items.length > 0 ? items[0] : fallback;
}
const count = firstOr([1, 2, 3], 0); // number, not number | string
// a named helper type keeps a signature readable… 11 more lines in the full lesson.
Full lesson: Generic patterns without over-engineering →
Typing asynchronous and external data
Promises and Awaited
interface User { id: string; name: string }
// an async function always returns a promise, so the annotation is the resolved value
async function loadUser(id: string): Promise<User> {
const response = await fetch('/api/users/' + id);
return (await response.json()) as User;
}
// Awaited unwraps nested promises, which a hand-written Promise<T> cannot
type Nested = Awaited<Promise<Promise<number>>>; // number
type Resolved = Awaited<ReturnType<typeof loadUser>>; // User
… 15 more lines in the full lesson.
Modelling failure without exceptions
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
// one wrapper turns any rejection into a value the caller must inspect
async function attempt<T>(work: () => Promise<T>): Promise<Result<T>> {
try {
return { ok: true, value: await work() };
} catch (error) {
return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
}
}… 15 more lines in the full lesson.
Full lesson: Typing asynchronous and external data →
Runtime validation at the edges
Types disappear at runtime
// each of these compiles, and none of them is checked at runtime
const user = JSON.parse(raw) as User; // a claim the compiler believes
const env = process.env as Record<string, string>; // missing keys are undefined
declare const settings: Settings; // trust me - no code is emitted
// the honest version: accept unknown, then prove the shape
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) return false;
const candidate = value as { id?: unknown; name?: unknown };
return typeof candidate.id === 'string' && typeof candidate.name === 'string';
}
… 9 more lines in the full lesson.
Full lesson: Runtime validation at the edges →
Declaration files and third-party types
Publishing types with a package
// emit declarations alongside the JavaScript
// tsc --declaration --emitDeclarationOnly --outDir dist
// one entry point re-exports the public surface
export { loadUser, type User } from './user.js';
export { parsePort } from './port.js';
// an explicit public API keeps internals unimportable, so refactors
// inside the package are not breaking changes for consumers
// verify the shipped types resolve the way consumers will resolve them
// npx @arethetypeswrong/cli --pack .
Types for someone else's package
// src/types/legacy-chart.d.ts - type an untyped dependency locally,
// without waiting for an upstream fix or publishing a fork
declare module 'legacy-chart' {
export interface Options {
width: number;
height: number;
theme?: 'light' | 'dark';
}
export function render(target: string, options: Options): void;
}
… 8 more lines in the full lesson.
Full lesson: Declaration files and third-party types →
Migrating a JavaScript project incrementally
Suppressions as tracked debt
// a suppression is a debt note: it says "known, not fixed"
const payload = JSON.parse(raw) as any; // bad: any leaks into every caller
// acceptable: constrain the escape hatch to one expression, with a reason
// @ts-expect-error third-party types are wrong here; upstream issue #412
legacyClient.send(payload);
// replace a real any with unknown as soon as the callers can handle it
function parse(raw: string): unknown { return JSON.parse(raw); }
// count the debt so it cannot grow unnoticed
// grep -rn "@ts-expect-error" src | wc -l… 2 more lines in the full lesson.
Full lesson: Migrating a JavaScript project incrementally →
FAQ
Is this TypeScript cheat sheet free to use?
Where do the examples come from?
How do I go deeper than a cheat sheet?
Related cheat sheets
HTML CSS JavaScript HTML DOM AJAX JSON
Last refreshed 2026-09-27.