TypeScript cheatsheet
Save this file from here if you want a copy.
================================================================================
TYPESCRIPT-CHEATSHEET.TXT
================================================================================
NAME
typescript-cheatsheet.txt -- offline TypeScript reference manual
SYNOPSIS
grep -A 40 "^SECTION_NAME" typescript-cheatsheet.txt
less typescript-cheatsheet.txt
grep -n "SECTION_NAME" typescript-cheatsheet.txt
DESCRIPTION
This file is a single flat plain-text reference manual covering the
TypeScript type system: the compiler and tsconfig, basic and
advanced types, interfaces, generics, narrowing, conditional and
mapped types, utility types, declaration files, and TypeScript as
applied to React, Node, and Express.
It covers ONLY what TypeScript adds. Runtime behavior, standard
library methods, algorithms, and everything else about the language
live in javascript-cheatsheet.txt. Every example there is valid
TypeScript.
It is written to be grepped, not rendered. Every major topic has a
large banner header made of '=' characters. Every subsection inside
a topic has a smaller banner made of '-' characters. There is no
markdown, no bullets unless natural, and no code fences. Code is
always indented four spaces.
Standard subsection names, greppable across the whole file:
DESCRIPTION
CREATE
COMMON METHODS
EXAMPLES
COMMON MISTAKES
INTERVIEW NOTES
SUGGESTED ALIAS
Add something like this to your shell rc file:
tsts() {
if [ -z "$1" ]; then
less ~/typescript-cheatsheet.txt
else
grep -n -i --color=always "$*" ~/typescript-cheatsheet.txt | less -R
fi
}
Usage:
tsts GENERICS
tsts CREATE
tsts UTILITY TYPES
tsts NARROWING
tsts TSCONFIG
Since every section header is a unique all-caps banner line, grep -n
will point you straight at the line number, and you can jump there
directly with:
less +<line number> ~/typescript-cheatsheet.txt
TABLE OF CONTENTS
MENTAL MODEL
TSC CLI
TSCONFIG
BASIC TYPES
ANY UNKNOWN NEVER VOID
ARRAYS AND TUPLES
OBJECT TYPES
INTERFACES
TYPE ALIASES
INTERFACE VS TYPE
UNIONS AND INTERSECTIONS
LITERAL TYPES
ENUMS
NARROWING
TYPE GUARDS
DISCRIMINATED UNIONS
FUNCTIONS
CLASSES
GENERICS
KEYOF TYPEOF INDEXED ACCESS
CONDITIONAL TYPES
MAPPED TYPES
TEMPLATE LITERAL TYPES
UTILITY TYPES
TYPE ASSERTIONS
SATISFIES
MODULES AND DECLARATION FILES
ASYNC TYPES
ERROR HANDLING
REACT
NODE AND EXPRESS
RUNTIME VALIDATION
TESTING
MIGRATING FROM JAVASCRIPT
COMMON COMPILER ERRORS
BEST PRACTICES
================================================================================
MENTAL MODEL
================================================================================
DESCRIPTION
Coming from C#, three differences matter more than any syntax detail.
-------------------------------------------------------------------------------
TYPES ARE ERASED
-------------------------------------------------------------------------------
TypeScript compiles to JavaScript by deleting every type. Nothing about
the type system exists at runtime. There is no reflection, no typeof
for a type, no generic type available inside a generic function.
function f<T>(x: T) {
// there is no way to ask what T is here
}
This is the opposite of C#, where generics are reified and
typeof(T) works. Anything you need at runtime must be a runtime value:
a string tag, a schema, a constructor reference.
-------------------------------------------------------------------------------
STRUCTURAL TYPING
-------------------------------------------------------------------------------
C# is nominal: a type matches only if it declares it implements the
interface. TypeScript is structural: a type matches if its shape fits.
interface Point { x: number; y: number; }
const p = { x: 1, y: 2, z: 3 };
const q: Point = p; // fine, it has x and y
You never write "implements" to satisfy an interface (though the
keyword exists on classes as an assertion). Shape is everything.
-------------------------------------------------------------------------------
THE TYPE SYSTEM IS NOT SOUND
-------------------------------------------------------------------------------
TypeScript deliberately allows some unsafe things for pragmatism: any,
type assertions, array index access returning T instead of T |
undefined by default, and method parameter bivariance.
It is a productivity tool that catches most mistakes, not a proof
system. Turn on strict and noUncheckedIndexedAccess to close the
largest gaps.
================================================================================
TSC CLI
================================================================================
DESCRIPTION
tsc is the TypeScript compiler. Use it to typecheck (often with
--noEmit) and to emit JS when you are not using a bundler. It does not
bundle.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
npm install -D typescript
npx tsc --version
-------------------------------------------------------------------------------
INITIALIZE
-------------------------------------------------------------------------------
npx tsc --init
npx tsc --init --strict
-------------------------------------------------------------------------------
COMPILE
-------------------------------------------------------------------------------
npx tsc // uses tsconfig.json
npx tsc --watch // recompile on change
npx tsc --noEmit // type check only, no output
npx tsc file.ts // single file, ignores tsconfig
-------------------------------------------------------------------------------
USEFUL FLAGS
-------------------------------------------------------------------------------
--noEmit check only, for CI and pre-commit
--strict enable all strict checks
--target es2022
--module nodenext
--outDir dist
--declaration emit .d.ts files
--sourceMap
--incremental cache for faster rebuilds
--listFiles which files were included
--traceResolution debug module resolution
--showConfig print the effective config
-------------------------------------------------------------------------------
RUNNING TYPESCRIPT DIRECTLY
-------------------------------------------------------------------------------
node --experimental-strip-types main.ts // Node 22+
npx tsx main.ts // tsx, no config needed
npx ts-node main.ts // older, slower
-------------------------------------------------------------------------------
TYPICAL PACKAGE.JSON
-------------------------------------------------------------------------------
"scripts": {
"build": "tsc",
"dev": "tsx watch src/main.ts",
"typecheck": "tsc --noEmit",
"test": "vitest run"
}
-------------------------------------------------------------------------------
NOTES
-------------------------------------------------------------------------------
tsc does not bundle. For applications, a bundler (Vite, esbuild, swc)
handles transpilation and tsc --noEmit handles type checking. They are
separate jobs and running both is normal.
================================================================================
TSCONFIG
================================================================================
DESCRIPTION
tsconfig.json is the compiler's configuration. strict is the default you
want on a new project. module and moduleResolution must agree.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
-------------------------------------------------------------------------------
WHAT STRICT TURNS ON
-------------------------------------------------------------------------------
noImplicitAny untyped parameters become an error
strictNullChecks null and undefined are separate types
strictFunctionTypes contravariant parameter checking
strictBindCallApply type-checks bind/call/apply
strictPropertyInitialization class fields must be assigned
noImplicitThis this must have a known type
useUnknownInCatchVariables catch (e) is unknown, not any
alwaysStrict emits "use strict"
strictNullChecks is the single most valuable one. Without it, every
type silently includes null and undefined and the compiler catches
almost nothing useful.
-------------------------------------------------------------------------------
OPTIONS WORTH ENABLING BEYOND STRICT
-------------------------------------------------------------------------------
noUncheckedIndexedAccess
arr[0] becomes T | undefined, and obj[key] likewise. Annoying at
first, correct always. This is the biggest remaining soundness gap
that strict does not close.
exactOptionalPropertyTypes
{ a?: string } no longer accepts { a: undefined }. Distinguishes
"absent" from "explicitly undefined".
noImplicitOverride
requires the override keyword when overriding a base method,
matching C# behavior and catching renamed-base-method bugs.
-------------------------------------------------------------------------------
PATH ALIASES
-------------------------------------------------------------------------------
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
import { Button } from "@/components/Button";
tsc resolves these for type checking but does NOT rewrite them in
output. The bundler or runtime must be configured to match (Vite
resolve.alias, tsconfig-paths for Node).
-------------------------------------------------------------------------------
PROJECT REFERENCES
-------------------------------------------------------------------------------
For monorepos, split into sub-projects that build independently:
{
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" }
]
}
npx tsc --build
Requires "composite": true in each referenced project.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Leaving strict off on a new project. It is far harder to turn on later.
Setting skipLibCheck: false and then fighting errors inside third-party
.d.ts files you cannot change. skipLibCheck: true is the sane default.
Mismatching module and moduleResolution (for example module: ESNext
with moduleResolution: Node10), which produces confusing import errors.
Forgetting that tsconfig include/exclude affects which files are
checked, so a file outside include silently gets no checking at all.
================================================================================
BASIC TYPES
================================================================================
DESCRIPTION
The primitive types, inference, const assertions, and the places you
actually write annotations. Prefer inference; annotate parameters and
public returns.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
let s: string = "hello";
let n: number = 42; // one number type, no int/float/decimal
let b: boolean = true;
let big: bigint = 10n;
let sym: symbol = Symbol("id");
let u: undefined = undefined;
let nl: null = null;
Always lowercase. String, Number, Boolean are the wrapper object types
and are almost never what you want.
-------------------------------------------------------------------------------
INFERENCE
-------------------------------------------------------------------------------
let x = 42; // number
const y = 42; // 42, a literal type
let name = "Jake"; // string
const arr = [1, 2, 3]; // number[]
const obj = { a: 1 }; // { a: number }
Do not annotate what is already inferred. Annotate function parameters,
function return types on public APIs, and empty containers.
const items = []; // any[], implicitly
const items: string[] = []; // annotate
-------------------------------------------------------------------------------
CONST ASSERTIONS
-------------------------------------------------------------------------------
const config = { mode: "dark", retries: 3 } as const;
// { readonly mode: "dark"; readonly retries: 3 }
const roles = ["admin", "user"] as const;
// readonly ["admin", "user"]
type Role = typeof roles[number]; // "admin" | "user"
This is the standard way to derive a union type from a runtime array,
keeping one source of truth.
-------------------------------------------------------------------------------
TYPE ANNOTATION SITES
-------------------------------------------------------------------------------
let x: string;
function f(a: number, b: string): boolean { }
const g = (a: number): string => "";
class C { field: string; }
interface I { prop: number; }
const h = <T,>(x: T): T => x; // trailing comma needed in .tsx
================================================================================
ANY UNKNOWN NEVER VOID
================================================================================
DESCRIPTION
any disables checking. unknown is the safe top type. never is the empty
type. void is 'ignore the return'. These four are how TypeScript talks
about absence and escape hatches.
-------------------------------------------------------------------------------
ANY
-------------------------------------------------------------------------------
Disables all checking on that value, and spreads: anything derived from
an any is also any. It is an escape hatch, not a type.
const data: any = JSON.parse(s);
data.whatever.at.all; // no error, may crash
Use it only to unblock a migration, and leave a TODO.
-------------------------------------------------------------------------------
UNKNOWN
-------------------------------------------------------------------------------
The safe version of any. Anything is assignable TO unknown, but unknown
is assignable to nothing without narrowing first.
const data: unknown = JSON.parse(s);
data.name; // error
if (typeof data === "object" && data !== null && "name" in data) {
// now usable
}
Use unknown for: JSON.parse results, catch variables, external API
responses, and anything you must validate before trusting.
-------------------------------------------------------------------------------
NEVER
-------------------------------------------------------------------------------
The type with no values. Produced by a function that never returns, by
an impossible narrowing, and by an empty union.
function fail(msg: string): never { throw new Error(msg); }
Its most useful role is exhaustiveness checking:
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.r ** 2;
case "square": return shape.side ** 2;
default: {
const exhaustive: never = shape;
throw new Error(`unhandled: ${exhaustive}`);
}
}
}
Add a new member to Shape and this fails to compile until handled. It
is the closest TypeScript comes to a sealed hierarchy with exhaustive
matching.
-------------------------------------------------------------------------------
VOID
-------------------------------------------------------------------------------
The return type of a function that returns nothing useful.
function log(msg: string): void { console.log(msg); }
void as a return type in a callback position means "the return value is
ignored", so a function returning something is still assignable:
const nums = [1, 2, 3];
nums.forEach((n) => map.set(n, n)); // set returns Map, fine
-------------------------------------------------------------------------------
NULL AND UNDEFINED
-------------------------------------------------------------------------------
With strictNullChecks, they are distinct types and must be handled:
function f(name: string | null) {
name.length; // error
name?.length; // number | undefined
if (name) name.length; // narrowed to string
(name ?? "").length; // number
}
Optional property vs union with undefined:
interface A { x?: number; } // may be absent
interface B { x: number | undefined; } // must be present
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using any to silence an error rather than understanding it. It usually
moves the failure to runtime.
Typing a catch variable as any (or leaving it, pre-strict) and calling
err.message. With useUnknownInCatchVariables it is unknown, and you
must narrow:
catch (err) {
const msg = err instanceof Error ? err.message : String(err);
}
Confusing never and void. void means "returns nothing"; never means
"does not return at all".
================================================================================
ARRAYS AND TUPLES
================================================================================
DESCRIPTION
T[] / Array<T> is a homogeneous list. A tuple is a fixed-length array
with per-index types. noUncheckedIndexedAccess makes indexing honest.
-------------------------------------------------------------------------------
ARRAYS
-------------------------------------------------------------------------------
let a: number[] = [1, 2, 3];
let b: Array<number> = [1, 2, 3]; // equivalent
let c: (string | number)[] = [1, "a"];
let d: readonly number[] = [1, 2, 3];
let e: ReadonlyArray<number> = [1, 2, 3];
readonly arrays reject push, pop, splice, sort, and index assignment at
compile time.
-------------------------------------------------------------------------------
TUPLES
-------------------------------------------------------------------------------
let pair: [string, number] = ["a", 1];
let named: [name: string, age: number] = ["Jake", 30];
let optional: [string, number?] = ["a"];
let rest: [string, ...number[]] = ["a", 1, 2, 3];
let frozen: readonly [number, number] = [1, 2];
Tuples are how useState-style returns are typed:
function useToggle(): [boolean, () => void] { }
-------------------------------------------------------------------------------
NOUNCHECKEDINDEXEDACCESS
-------------------------------------------------------------------------------
Without it:
const first = arr[0]; // number, even if arr is empty
With it:
const first = arr[0]; // number | undefined, correct
Handle it with a guard, .at(), or a non-null assertion where you have
genuinely proven it:
const first = arr[0];
if (first === undefined) return;
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Annotating an array of objects as any[] instead of defining the element
type, losing all checking inside map and filter callbacks.
Assuming a tuple keeps its tuple-ness after a method call. map on a
tuple returns an array, not a tuple, unless typed with a mapped tuple.
Expecting readonly to be deep. readonly T[] only prevents mutating the
array, not the objects inside it.
================================================================================
OBJECT TYPES
================================================================================
DESCRIPTION
Object types describe shapes: required and optional properties,
readonly, index signatures, and excess-property checking on fresh
literals.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
let user: { name: string; age: number };
function greet(p: { name: string }): string { return p.name; }
-------------------------------------------------------------------------------
MODIFIERS
-------------------------------------------------------------------------------
interface User {
readonly id: number; // cannot be reassigned after creation
name: string;
age?: number; // optional: number | undefined
[key: string]: unknown; // index signature
}
-------------------------------------------------------------------------------
INDEX SIGNATURES
-------------------------------------------------------------------------------
interface StringMap { [key: string]: string; }
interface NumberMap { [index: number]: string; }
Every declared property must be assignable to the index signature type,
which is why mixing them often fails:
interface Bad {
[key: string]: string;
count: number; // error
}
Prefer Record<K, V> for the common case, and a Map for dynamic keys.
-------------------------------------------------------------------------------
EXCESS PROPERTY CHECKING
-------------------------------------------------------------------------------
Object literals assigned directly get an extra check that variables do
not:
interface Point { x: number; y: number; }
const p: Point = { x: 1, y: 2, z: 3 }; // error, z is excess
const temp = { x: 1, y: 2, z: 3 };
const q: Point = temp; // fine, structural
This is deliberate: a literal with an unexpected key is almost always a
typo, while an existing wider object being narrowed is normal.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Being surprised that an object with extra properties is assignable
through a variable but not as a literal. See above; it is by design.
Using an index signature to avoid defining real keys, which throws away
autocomplete and typo detection.
================================================================================
INTERFACES
================================================================================
DESCRIPTION
interface names an object shape that can be extended and merged. Use it
for public object APIs; use type for unions and computed types.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
interface User {
id: number;
name: string;
greet(): string; // method
onChange: (value: string) => void; // property holding a function
}
-------------------------------------------------------------------------------
EXTENDING
-------------------------------------------------------------------------------
interface Admin extends User {
level: number;
}
interface Both extends User, Timestamped { }
-------------------------------------------------------------------------------
DECLARATION MERGING
-------------------------------------------------------------------------------
Two interfaces with the same name in the same scope merge:
interface Window { myApp: App; }
This is how you augment third-party types (Express Request, the global
Window). Type aliases cannot do this.
-------------------------------------------------------------------------------
IMPLEMENTING IN A CLASS
-------------------------------------------------------------------------------
class UserImpl implements User {
constructor(public id: number, public name: string) {}
greet() { return `Hi, ${this.name}`; }
onChange = (value: string) => {};
}
implements is only an assertion that the class satisfies the shape. It
does not change the class's type, and it is not required for the class
to be assignable to the interface (structural typing).
-------------------------------------------------------------------------------
CALL AND CONSTRUCT SIGNATURES
-------------------------------------------------------------------------------
interface Formatter {
(value: string): string; // callable
locale: string; // and has properties
}
interface UserConstructor {
new (name: string): User; // constructible
}
================================================================================
TYPE ALIASES
================================================================================
DESCRIPTION
type names any type: unions, primitives, tuples, functions, mapped and
conditional types. Interfaces can only describe object shapes.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
type ID = string | number;
type Point = { x: number; y: number };
type Handler = (event: Event) => void;
type Pair<T> = [T, T];
type Nullable<T> = T | null;
type Keys = keyof Point;
type Tree<T> = { value: T; children: Tree<T>[] }; // recursive is OK
Aliases can name anything: unions, primitives, tuples, functions,
conditional and mapped types. Interfaces can only describe object
shapes.
================================================================================
INTERFACE VS TYPE
================================================================================
DESCRIPTION
Both can describe object shapes, be generic, and be implemented. Only
interface merges. Only type aliases unions, mapped types, and infer.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
Both can:
describe object shapes
be extended (extends vs intersection)
be implemented by a class
be generic
Only interface can:
merge across declarations (module augmentation)
produce slightly better error messages in some editors
Only type can:
alias a union, primitive, tuple, or function type directly
use conditional, mapped, and template literal types
use the infer keyword
Practical rule: use interface for object shapes that other code
implements or extends, especially public API surfaces. Use type for
everything else, particularly unions and computed types. Consistency
within a codebase matters more than the choice.
================================================================================
UNIONS AND INTERSECTIONS
================================================================================
DESCRIPTION
A union is A | B (one of). An intersection is A & B (both). Unions of
objects usually need a discriminant to narrow.
-------------------------------------------------------------------------------
UNIONS
-------------------------------------------------------------------------------
type Result = string | number;
type Status = "idle" | "loading" | "error";
Only members common to every branch are accessible without narrowing:
function f(x: string | number) {
x.toString(); // fine, both have it
x.toUpperCase(); // error, number does not
}
-------------------------------------------------------------------------------
INTERSECTIONS
-------------------------------------------------------------------------------
type Timestamped = { createdAt: Date };
type User = { name: string };
type StampedUser = User & Timestamped; // has both
An intersection of conflicting primitive types is never:
type Impossible = string & number; // never
-------------------------------------------------------------------------------
UNION OF OBJECTS
-------------------------------------------------------------------------------
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
This is a discriminated union; see that section. It is the single most
useful pattern in the language.
-------------------------------------------------------------------------------
DISTRIBUTIVITY
-------------------------------------------------------------------------------
A conditional type over a naked type parameter distributes across a
union:
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
Prevent distribution by wrapping in a tuple:
type NoDist<T> = [T] extends [any] ? T[] : never;
type R2 = NoDist<string | number>; // (string | number)[]
================================================================================
LITERAL TYPES
================================================================================
DESCRIPTION
A type that is one specific string, number, or boolean rather than the
widened primitive. as const and unions of literals are the usual
sources.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
type Direction = "up" | "down" | "left" | "right";
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Flag = true;
let d: Direction = "up";
d = "sideways"; // error
Widening:
let a = "up"; // string, widened
const b = "up"; // "up", literal
let c: Direction = "up"; // "up", annotated
Deriving a union from a runtime array keeps one source of truth:
const DIRECTIONS = ["up", "down"] as const;
type Direction = typeof DIRECTIONS[number];
This pattern is usually better than an enum; see the next section.
================================================================================
ENUMS
================================================================================
DESCRIPTION
TypeScript enums exist at runtime (except const enum). A union of string
literals plus as const is usually the better choice.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
enum Status { Idle, Loading, Done } // 0, 1, 2
enum Level { Low = 1, Mid = 5, High = 10 }
enum Role {
Admin = "ADMIN",
User = "USER"
}
-------------------------------------------------------------------------------
WHY TO AVOID THEM
-------------------------------------------------------------------------------
Enums are one of the few TypeScript features that emit runtime code,
which breaks the "types are erased" model and prevents isolatedModules
tooling from stripping types cleanly. Numeric enums also accept any
number in older versions, and are not type-safe in the way the name
suggests.
The modern replacement:
const Role = { Admin: "ADMIN", User: "USER" } as const;
type Role = typeof Role[keyof typeof Role]; // "ADMIN" | "USER"
You get autocomplete, exhaustiveness, no runtime object beyond a plain
frozen literal, and the values are just strings in the JSON you send.
-------------------------------------------------------------------------------
CONST ENUMS
-------------------------------------------------------------------------------
const enum Direction { Up, Down }
Inlined at the call site with no runtime object, but incompatible with
isolatedModules and with most bundler setups. Avoid.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
"When would you use an enum" is a reasonable filter question. The
strong answer names the runtime-emit problem and the as-const union
alternative, rather than treating enums as the obvious C#-style choice.
================================================================================
NARROWING
================================================================================
DESCRIPTION
Narrowing is how TypeScript refines a broad type to a specific one
inside a block. Control flow analysis does this automatically.
-------------------------------------------------------------------------------
TYPEOF
-------------------------------------------------------------------------------
function f(x: string | number) {
if (typeof x === "string") x.toUpperCase();
else x.toFixed(2);
}
Remember typeof null is "object", so a typeof check does not exclude
null from an object type.
-------------------------------------------------------------------------------
TRUTHINESS
-------------------------------------------------------------------------------
function f(s?: string) {
if (s) s.length; // string
}
Careful: this also excludes "" and 0, which may be valid values. Prefer
an explicit check when they are:
if (s !== undefined) { }
-------------------------------------------------------------------------------
EQUALITY
-------------------------------------------------------------------------------
function f(x: string | number, y: string | boolean) {
if (x === y) {
// both narrowed to string
}
}
if (x != null) { } // removes both null and undefined
-------------------------------------------------------------------------------
INSTANCEOF
-------------------------------------------------------------------------------
if (err instanceof ValidationError) err.field;
-------------------------------------------------------------------------------
IN OPERATOR
-------------------------------------------------------------------------------
type Fish = { swim(): void };
type Bird = { fly(): void };
function move(animal: Fish | Bird) {
if ("swim" in animal) animal.swim();
else animal.fly();
}
-------------------------------------------------------------------------------
ARRAY.ISARRAY
-------------------------------------------------------------------------------
function f(x: string | string[]) {
if (Array.isArray(x)) x.join(",");
else x.toUpperCase();
}
-------------------------------------------------------------------------------
ASSIGNMENT AND CONTROL FLOW
-------------------------------------------------------------------------------
let x: string | number = "a";
x.toUpperCase(); // narrowed to string by assignment
x = 42;
x.toFixed(); // now number
Early return narrows the rest of the function:
function f(u: User | null) {
if (!u) return;
u.name; // User
}
-------------------------------------------------------------------------------
WHERE NARROWING IS LOST
-------------------------------------------------------------------------------
Inside a callback, because TypeScript cannot know when it runs:
if (this.user) {
setTimeout(() => this.user.name); // error
}
const user = this.user;
if (user) setTimeout(() => user.name); // fine
After any function call, if the narrowed value is a mutable property of
an object, since the call could have reassigned it. Copy to a const
first; this is the standard fix.
================================================================================
TYPE GUARDS
================================================================================
DESCRIPTION
A function whose return type is a type predicate (x is T) or an
assertion (asserts x is T). That is how you teach the checker a custom
check.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: unknown): x is User {
return (
typeof x === "object" &&
x !== null &&
"id" in x &&
typeof (x as User).id === "number"
);
}
The "x is T" return type tells the compiler to narrow at the call site.
It is an assertion you are responsible for getting right; a wrong guard
silently lies to the type system.
-------------------------------------------------------------------------------
FILTERING WITH A GUARD
-------------------------------------------------------------------------------
const maybe: (string | null)[] = ["a", null, "b"];
const strings = maybe.filter((x): x is string => x !== null);
// string[]
Without the guard annotation, filter returns (string | null)[].
A reusable version:
function isDefined<T>(x: T | null | undefined): x is T {
return x != null;
}
const clean = maybe.filter(isDefined);
-------------------------------------------------------------------------------
ASSERTION FUNCTIONS
-------------------------------------------------------------------------------
function assertIsString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new TypeError("expected string");
}
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg ?? "assertion failed");
}
const value: unknown = getValue();
assertIsString(value);
value.toUpperCase(); // narrowed from here on
An assertion function must have an explicit type annotation on the
variable holding it if assigned to a const, which is a known rough
edge.
================================================================================
DISCRIMINATED UNIONS
================================================================================
DESCRIPTION
A union of object types sharing a common literal-typed property. This
is the workhorse pattern of TypeScript and replaces most places where
C# would use inheritance and polymorphism.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; w: number; h: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.w * shape.h;
}
}
Inside each case, the union is narrowed to exactly one member and its
unique properties are accessible.
-------------------------------------------------------------------------------
EXHAUSTIVENESS
-------------------------------------------------------------------------------
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
default: {
const _exhaustive: never = shape;
throw new Error(`unhandled kind: ${JSON.stringify(shape)}`);
}
}
}
Adding a new member to Shape now fails to compile at every switch that
does not handle it. This is the main reason to prefer unions over
optional-property grab-bag objects.
-------------------------------------------------------------------------------
ASYNC STATE
-------------------------------------------------------------------------------
type State<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
This makes impossible states impossible: you cannot have data and an
error at the same time, and you cannot read data without first checking
the status.
-------------------------------------------------------------------------------
RESULT TYPE
-------------------------------------------------------------------------------
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const res = await fetch(url);
if (!res.ok) return { ok: false, error: new Error(`${res.status}`) };
return { ok: true, value: (await res.json()) as T };
} catch (err) {
return { ok: false, error: err as Error };
}
}
const result = await safeFetch<User>("/api/me");
if (result.ok) use(result.value);
else handle(result.error);
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Using a boolean as the discriminant with more than two states, or using
a non-literal type (string instead of "circle" | "square"), which
defeats narrowing entirely.
Building one object type with many optional properties instead of a
union, then checking for undefined everywhere. The union expresses the
actual states.
================================================================================
FUNCTIONS
================================================================================
DESCRIPTION
Parameter, return, this, overload, and rest typing. Annotate the
function type or the parameters; do not do both unless they disagree on
purpose.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
function add(a: number, b: number): number { return a + b; }
const add = (a: number, b: number): number => a + b;
type BinaryOp = (a: number, b: number) => number;
const add: BinaryOp = (a, b) => a + b; // params inferred from type
Annotating the variable rather than the parameters is often cleaner and
is called contextual typing.
-------------------------------------------------------------------------------
OPTIONAL, DEFAULT, REST
-------------------------------------------------------------------------------
function f(a: number, b?: number): void { } // b: number | undefined
function g(a: number, b: number = 10): void { } // b: number
function h(...nums: number[]): number { }
function k(first: string, ...rest: number[]): void { }
Optional parameters must follow required ones.
-------------------------------------------------------------------------------
OBJECT PARAMETERS
-------------------------------------------------------------------------------
interface CreateUserOptions {
name: string;
age?: number;
isAdmin?: boolean;
}
function createUser({ name, age = 0, isAdmin = false }: CreateUserOptions) { }
This is the TypeScript equivalent of C# named and optional arguments,
and the standard shape for any function with more than three inputs.
-------------------------------------------------------------------------------
OVERLOADS
-------------------------------------------------------------------------------
function parse(input: string): object;
function parse(input: string, asArray: true): unknown[];
function parse(input: string, asArray?: boolean): object | unknown[] {
const result = JSON.parse(input);
return asArray ? [result].flat() : result;
}
The implementation signature is not callable from outside; only the
overload signatures are. Prefer a union return or generics when they
express the same thing, since overloads are harder to maintain.
-------------------------------------------------------------------------------
THIS PARAMETER
-------------------------------------------------------------------------------
function handler(this: HTMLButtonElement, e: MouseEvent): void {
this.disabled = true;
}
A fake first parameter, erased at compile time, that types this.
-------------------------------------------------------------------------------
RETURN TYPE NOTES
-------------------------------------------------------------------------------
Let return types be inferred inside a module; annotate them on exported
functions. An annotation is a contract that catches accidental widening
and produces better errors at the definition rather than at every call
site.
A function returning a type predicate (x is T) or asserts must always
be annotated; it cannot be inferred.
================================================================================
CLASSES
================================================================================
DESCRIPTION
Typed fields, parameter properties, visibility, abstract classes,
implements, and generic classes. strictPropertyInitialization is the
usual gotcha.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
class User {
readonly id: number;
name: string;
protected role: string = "user";
private secret: string;
static count = 0;
#trulyPrivate = 1; // runtime private, not just compile
constructor(id: number, name: string, secret: string) {
this.id = id;
this.name = name;
this.secret = secret;
User.count++;
}
greet(): string { return `Hi, ${this.name}`; }
get display(): string { return this.name.toUpperCase(); }
set display(v: string) { this.name = v.toLowerCase(); }
}
-------------------------------------------------------------------------------
PARAMETER PROPERTIES
-------------------------------------------------------------------------------
class User {
constructor(
public readonly id: number,
public name: string,
private secret: string
) {}
}
A modifier on a constructor parameter declares and assigns the field in
one step. This is TypeScript-only syntax with no JavaScript equivalent.
-------------------------------------------------------------------------------
ACCESS MODIFIERS
-------------------------------------------------------------------------------
public default, accessible anywhere
protected this class and subclasses
private this class only, COMPILE TIME ONLY
#field true runtime privacy, enforced by the engine
readonly assignable only in the constructor or initializer
private is erased. At runtime the property is a normal property and can
be reached with obj["secret"]. Use #field when privacy must actually
hold, for example when handling secrets or preventing external
tampering.
-------------------------------------------------------------------------------
ABSTRACT CLASSES
-------------------------------------------------------------------------------
abstract class Shape {
abstract area(): number;
abstract readonly kind: string;
describe(): string { return `${this.kind}: ${this.area()}`; }
}
class Circle extends Shape {
readonly kind = "circle";
constructor(private r: number) { super(); }
area(): number { return Math.PI * this.r ** 2; }
}
Unlike interfaces, abstract classes exist at runtime and can hold
implementation. Unlike C#, you usually reach for a discriminated union
instead.
-------------------------------------------------------------------------------
IMPLEMENTS AND OVERRIDE
-------------------------------------------------------------------------------
class Service implements IService { }
class Derived extends Base {
override handle(): void { } // required with noImplicitOverride
}
-------------------------------------------------------------------------------
GENERIC CLASSES
-------------------------------------------------------------------------------
class Repository<T extends { id: number }> {
#items = new Map<number, T>();
add(item: T): void { this.#items.set(item.id, item); }
find(id: number): T | undefined { return this.#items.get(id); }
all(): T[] { return [...this.#items.values()]; }
}
const users = new Repository<User>();
-------------------------------------------------------------------------------
STRICT PROPERTY INITIALIZATION
-------------------------------------------------------------------------------
Under strict, every declared field must be assigned in the constructor
or have an initializer. When it is assigned elsewhere (a framework,
a lifecycle hook), use the definite assignment assertion:
class C {
value!: string; // "trust me, it gets set"
}
Use this sparingly; it is a promise the compiler cannot check.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Assuming private means private at runtime. It does not.
Reaching for class hierarchies out of C# habit where a discriminated
union or a factory function would be simpler and easier to serialize.
Forgetting that classes are compared structurally, so two unrelated
classes with the same shape are mutually assignable. A private field
is the usual way to force nominal-like behavior:
class Brand { private __brand?: never; }
================================================================================
GENERICS
================================================================================
DESCRIPTION
A type parameter is a placeholder the caller fills in. Constrain it when
you need properties; default it when the common case is obvious.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
function identity<T>(value: T): T { return value; }
identity<string>("a"); // explicit
identity("a"); // inferred, preferred
Multiple parameters:
function pair<K, V>(key: K, value: V): [K, V] { return [key, value]; }
-------------------------------------------------------------------------------
CONSTRAINTS
-------------------------------------------------------------------------------
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("abc", "de"); // string
longest([1, 2], [3]); // number[]
longest(1, 2); // error, number has no length
-------------------------------------------------------------------------------
KEY CONSTRAINTS
-------------------------------------------------------------------------------
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Jake", age: 30 };
getProp(user, "name"); // string
getProp(user, "nope"); // error
This is the single most useful generic pattern in application code.
-------------------------------------------------------------------------------
DEFAULTS
-------------------------------------------------------------------------------
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
-------------------------------------------------------------------------------
GENERIC CONSTRAINTS ON CONSTRUCTORS
-------------------------------------------------------------------------------
type Constructor<T = {}> = new (...args: any[]) => T;
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
};
}
class User {}
const TimestampedUser = Timestamped(User);
This is the mixin pattern, TypeScript's answer to multiple inheritance.
-------------------------------------------------------------------------------
WHEN NOT TO USE GENERICS
-------------------------------------------------------------------------------
A type parameter used only once in the signature is usually a mistake:
function f<T>(x: T): void { } // T adds nothing, use unknown
The rule of thumb: a generic earns its place when it relates two or
more positions (a parameter to the return type, or two parameters to
each other).
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Expecting T to exist at runtime. It does not; types are erased. If you
need runtime behavior per type, pass a value (a schema, a constructor,
a tag string).
Over-constraining with any: <T extends any> is not a constraint.
Adding explicit type arguments everywhere when inference already works,
which makes refactoring harder.
================================================================================
KEYOF TYPEOF INDEXED ACCESS
================================================================================
DESCRIPTION
keyof T is the union of keys. typeof value captures a type from a value.
T[K] looks up a property type. Together they type dynamic access.
-------------------------------------------------------------------------------
KEYOF
-------------------------------------------------------------------------------
interface User { id: number; name: string; }
type UserKey = keyof User; // "id" | "name"
type AnyKey = keyof any; // string | number | symbol
-------------------------------------------------------------------------------
TYPEOF (TYPE POSITION)
-------------------------------------------------------------------------------
const config = { host: "localhost", port: 3000 };
type Config = typeof config; // { host: string; port: number }
type ConfigKey = keyof typeof config; // "host" | "port"
typeof in a type position queries the type of a VALUE. It is unrelated
to the runtime typeof operator despite the shared keyword.
-------------------------------------------------------------------------------
INDEXED ACCESS
-------------------------------------------------------------------------------
type Name = User["name"]; // string
type Either = User["id" | "name"]; // number | string
type Values = User[keyof User]; // number | string
Array element type:
type Item = typeof items[number];
Function return and parameter types:
type R = ReturnType<typeof loadUser>;
type P = Parameters<typeof loadUser>[0];
-------------------------------------------------------------------------------
THE AS-CONST PIPELINE
-------------------------------------------------------------------------------
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin"|"editor"|"viewer"
const ROUTES = {
home: "/",
user: "/users/:id"
} as const;
type RouteName = keyof typeof ROUTES; // "home" | "user"
type RoutePath = typeof ROUTES[RouteName]; // "/" | "/users/:id"
One runtime source of truth, types derived from it. This combination is
worth memorizing; it comes up constantly.
================================================================================
CONDITIONAL TYPES
================================================================================
DESCRIPTION
T extends U ? X : Y. Combined with infer they extract types from other
types. They distribute over naked unions.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
-------------------------------------------------------------------------------
INFER
-------------------------------------------------------------------------------
infer captures a type from within a pattern:
type ElementOf<T> = T extends (infer U)[] ? U : never;
type E = ElementOf<string[]>; // string
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type U = Unwrap<Promise<number>>; // number
type Ret<T> = T extends (...args: any[]) => infer R ? R : never;
type First<T> = T extends [infer F, ...any[]] ? F : never;
-------------------------------------------------------------------------------
PRACTICAL EXAMPLES
-------------------------------------------------------------------------------
Deep partial:
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
Non-nullable:
type NonNull<T> = T extends null | undefined ? never : T;
Function arity:
type Awaited1<T> = T extends PromiseLike<infer U> ? Awaited1<U> : T;
Filtering a union:
type Extract2<T, U> = T extends U ? T : never;
type Exclude2<T, U> = T extends U ? never : T;
-------------------------------------------------------------------------------
NOTE
-------------------------------------------------------------------------------
Conditional types are where TypeScript becomes a small functional
language. They are essential for library authors and rarely needed in
application code. If a conditional type in application code is more
than three lines, there is usually a simpler shape available.
================================================================================
MAPPED TYPES
================================================================================
DESCRIPTION
Build a new object type by iterating keys: { [K in keyof T]: ... }. The
as clause remaps keys. This is how Partial, Pick, and Readonly are
written.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
type Optional<T> = { [K in keyof T]?: T[K] };
type Immutable<T> = { readonly [K in keyof T]: T[K] };
type Stringify<T> = { [K in keyof T]: string };
-------------------------------------------------------------------------------
MODIFIERS
-------------------------------------------------------------------------------
Add with + (implicit), remove with -:
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Required2<T> = { [K in keyof T]-?: T[K] };
-------------------------------------------------------------------------------
KEY REMAPPING WITH AS
-------------------------------------------------------------------------------
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface User { name: string; age: number }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
Filter keys by remapping to never:
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
-------------------------------------------------------------------------------
MAPPING A UNION OF KEYS
-------------------------------------------------------------------------------
type Flags = { [K in "read" | "write"]: boolean };
// { read: boolean; write: boolean }
type Record2<K extends keyof any, V> = { [P in K]: V };
================================================================================
TEMPLATE LITERAL TYPES
================================================================================
DESCRIPTION
String types concatenated and inferred at the type level. Useful for
event names, CSS-ish unions, and extracting pieces of literal strings.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
type Greeting = `hello ${string}`;
type Event = `on${"Click" | "Hover"}`; // "onClick" | "onHover"
type CssUnit = `${number}px` | `${number}rem`;
Intrinsic string manipulation types:
Uppercase<"abc"> // "ABC"
Lowercase<"ABC"> // "abc"
Capitalize<"abc"> // "Abc"
Uncapitalize<"Abc"> // "abc"
Practical: typed event names and route params.
type Route = "/users/:id/posts/:postId";
type Params<T extends string> =
T extends `${string}:${infer P}/${infer Rest}`
? P | Params<`/${Rest}`>
: T extends `${string}:${infer P}`
? P
: never;
type R = Params<Route>; // "id" | "postId"
Powerful, but heavy. Deep recursive template types slow the compiler
noticeably in large projects.
================================================================================
UTILITY TYPES
================================================================================
DESCRIPTION
The built-in Partial, Required, Pick, Omit, Record, Exclude, Extract,
NonNullable, ReturnType, Parameters, Awaited, and a few you will write
yourself.
-------------------------------------------------------------------------------
COMMON METHODS
-------------------------------------------------------------------------------
Partial<T> all properties optional
Required<T> all properties required
Readonly<T> all properties readonly
Pick<T, K> keep only keys K
Omit<T, K> remove keys K
Record<K, V> object with keys K and values V
interface User { id: number; name: string; email: string }
type UserDraft = Partial<User>;
type UserSummary = Pick<User, "id" | "name">;
type PublicUser = Omit<User, "email">;
type UsersById = Record<number, User>;
type Flags = Record<"read" | "write", boolean>;
-------------------------------------------------------------------------------
UNION TRANSFORMS
-------------------------------------------------------------------------------
Exclude<T, U> remove members of T assignable to U
Extract<T, U> keep members of T assignable to U
NonNullable<T> remove null and undefined
type Status = "idle" | "loading" | "error";
type Settled = Exclude<Status, "loading">; // "idle" | "error"
-------------------------------------------------------------------------------
FUNCTION TYPES
-------------------------------------------------------------------------------
Parameters<F> tuple of parameter types
ReturnType<F> return type
ConstructorParameters<C>
InstanceType<C>
ThisParameterType<F>
OmitThisParameter<F>
Awaited<T> unwrap nested promises
type Args = Parameters<typeof createUser>;
type Res = Awaited<ReturnType<typeof loadUser>>;
The Awaited<ReturnType<typeof fn>> combination is how you get the
resolved type of an async function without exporting a separate type.
-------------------------------------------------------------------------------
STRING TYPES
-------------------------------------------------------------------------------
Uppercase<S> Lowercase<S> Capitalize<S> Uncapitalize<S>
-------------------------------------------------------------------------------
USEFUL CUSTOM UTILITIES
-------------------------------------------------------------------------------
Deep readonly:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
};
At least one key required:
type RequireAtLeastOne<T, K extends keyof T = keyof T> =
Omit<T, K> & { [P in K]-?: Required<Pick<T, P>> }[K];
Prettify (flattens intersections in tooltips, purely cosmetic but very
useful when debugging types):
type Prettify<T> = { [K in keyof T]: T[K] } & {};
Nominal branding:
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<number, "UserId">;
type PostId = Brand<number, "PostId">;
// UserId and PostId are no longer interchangeable
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Omit<T, K> does not check that K is a key of T, so a typo silently
omits nothing. Pick does check. This is a known wart.
Using Partial<T> on a function parameter and then treating every field
as present. Partial makes everything optional, including things your
code requires.
================================================================================
TYPE ASSERTIONS
================================================================================
DESCRIPTION
as T and postfix ! tell the checker to stop. Nothing happens at runtime.
Prefer a guard; comment the assertion when you cannot.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
const el = document.getElementById("app") as HTMLCanvasElement;
const el = <HTMLCanvasElement>document.getElementById("app"); // not in .tsx
An assertion tells the compiler to stop checking. It does not convert
anything at runtime. If you are wrong, it fails later and further away.
Non-null assertion:
const el = document.getElementById("app")!;
user!.profile!.email;
Double assertion when the types do not overlap (a code smell, but
sometimes necessary in tests):
const mock = {} as unknown as Database;
Prefer, in order:
1. fix the types so no assertion is needed
2. a type guard that actually checks
3. an assertion with a comment explaining why it is safe
================================================================================
SATISFIES
================================================================================
DESCRIPTION
satisfies checks a value against a type without widening the value's
inferred type. Use it for config objects and as const tables you still
want checked.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
satisfies checks a value against a type WITHOUT widening it.
const config = {
host: "localhost",
port: 3000
} satisfies Record<string, string | number>;
config.port.toFixed(); // number, still narrow
Compare:
const a: Record<string, string | number> = { host: "x", port: 1 };
a.port.toFixed(); // error, widened to string | number
The common use: validate a config or route map against a shape while
keeping the literal types for autocomplete.
const routes = {
home: "/",
user: "/users/:id"
} satisfies Record<string, `/${string}`>;
type RouteName = keyof typeof routes; // "home" | "user"
Rule of thumb: annotation for variables you will reassign, satisfies
for constant data you want both validated and precisely typed.
================================================================================
MODULES AND DECLARATION FILES
================================================================================
DESCRIPTION
Type-only imports, .d.ts files, module augmentation, and env typing.
isolatedModules wants import type when you only need the type.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
import type { User } from "./types";
import { type User, createUser } from "./api";
export type { User };
The type modifier guarantees the import is erased at compile time. It
is required under isolatedModules when importing a type from a module
whose runtime side you do not need, and it prevents accidental circular
runtime dependencies.
-------------------------------------------------------------------------------
DECLARATION FILES
-------------------------------------------------------------------------------
A .d.ts file contains types only, no implementation.
// types/env.d.ts
declare module "*.svg" {
const content: string;
export default content;
}
declare global {
interface Window {
analytics: Analytics;
}
}
export {}; // makes the file a module, required for declare global
-------------------------------------------------------------------------------
THIRD PARTY TYPES
-------------------------------------------------------------------------------
npm install -D @types/express
npm install -D @types/node
Modern packages ship their own types via a "types" field in
package.json. @types/* packages from DefinitelyTyped cover the rest.
For an untyped package, a minimal stub:
// types/untyped-lib.d.ts
declare module "untyped-lib" {
export function doThing(input: string): number;
}
-------------------------------------------------------------------------------
MODULE AUGMENTATION
-------------------------------------------------------------------------------
Add properties to an existing third-party type:
// types/express.d.ts
import "express";
declare module "express-serve-static-core" {
interface Request {
user?: { id: number; role: string };
}
}
This is why interfaces support declaration merging and type aliases do
not.
-------------------------------------------------------------------------------
ENVIRONMENT VARIABLES
-------------------------------------------------------------------------------
declare global {
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
PORT?: string;
NODE_ENV: "development" | "production" | "test";
}
}
}
This gives autocomplete on process.env but does NOT validate at
runtime. Validate with a schema at startup; see RUNTIME VALIDATION.
================================================================================
ASYNC TYPES
================================================================================
DESCRIPTION
async functions return Promise<T>. res.json() is Promise<any>; assign it
to unknown and validate. Awaited<T> unwraps promises.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<User>;
}
An async function must be annotated Promise<T>, not T.
Typing fetch results honestly:
async function loadUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
const data: unknown = await res.json();
return parseUser(data); // a real validator, see below
}
res.json() returns Promise<any>, which silently poisons everything
downstream. Assign it to unknown and validate.
Promise combinators keep their tuple types:
const [user, posts] = await Promise.all([
loadUser(1),
loadPosts(1)
]); // [User, Post[]]
const results = await Promise.allSettled([loadUser(1)]);
// PromiseSettledResult<User>[]
Unwrapping:
type UserType = Awaited<ReturnType<typeof loadUser>>; // User
================================================================================
ERROR HANDLING
================================================================================
DESCRIPTION
catch (err) is unknown under strict. Narrow with instanceof, or
normalize to Error. Typed failure belongs in the return type (a Result
union), not in a throws clause — TypeScript has none.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
With useUnknownInCatchVariables (part of strict), catch is unknown:
try {
risky();
} catch (err) {
if (err instanceof ValidationError) handle(err.field);
else if (err instanceof Error) log(err.message);
else log(String(err));
}
A reusable normalizer:
function toError(err: unknown): Error {
if (err instanceof Error) return err;
return new Error(typeof err === "string" ? err : JSON.stringify(err));
}
Custom error classes:
class HttpError extends Error {
constructor(
message: string,
public readonly status: number,
options?: ErrorOptions
) {
super(message, options);
this.name = "HttpError";
}
}
When targeting ES5, extending built-ins like Error breaks instanceof.
Target ES2015 or later, or add:
Object.setPrototypeOf(this, HttpError.prototype);
Typed result instead of throwing: see the Result type in DISCRIMINATED
UNIONS. TypeScript has no checked exceptions and no way to express
"this function throws E", so a Result union is the only way to make
failure visible in the signature.
================================================================================
REACT
================================================================================
DESCRIPTION
Props, native element extensions, hook types, events, and generic
components. The component's props type is the whole public API.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
children?: React.ReactNode;
}
function Button({ label, variant = "primary", onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
}
Prefer a plain annotated function over React.FC. React.FC adds an
implicit children prop (in older versions), complicates generics, and
buys nothing.
-------------------------------------------------------------------------------
EXTENDING NATIVE ELEMENT PROPS
-------------------------------------------------------------------------------
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
}
function Button({ variant = "primary", ...rest }: ButtonProps) {
return <button className={variant} {...rest} />;
}
Useful element prop types:
React.ComponentProps<"input">
React.ComponentProps<typeof MyComponent>
React.InputHTMLAttributes<HTMLInputElement>
React.HTMLAttributes<HTMLDivElement>
-------------------------------------------------------------------------------
HOOKS
-------------------------------------------------------------------------------
const [count, setCount] = useState(0); // number, inferred
const [user, setUser] = useState<User | null>(null); // annotate unions
const [items, setItems] = useState<Item[]>([]); // annotate empties
const ref = useRef<HTMLInputElement>(null); // for DOM refs
const timer = useRef<number | undefined>(undefined); // for values
const value = useMemo<Config>(() => build(deps), [deps]);
const cb = useCallback((id: number) => select(id), []);
Reducer:
type Action =
| { type: "increment" }
| { type: "set"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "set": return action.payload;
}
}
const [state, dispatch] = useReducer(reducer, 0);
Context:
const ThemeContext = createContext<Theme | undefined>(undefined);
function useTheme(): Theme {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within provider");
return ctx;
}
That throw is what turns Theme | undefined into Theme for every
consumer, and it is the standard pattern.
-------------------------------------------------------------------------------
EVENT TYPES
-------------------------------------------------------------------------------
React.MouseEvent<HTMLButtonElement>
React.ChangeEvent<HTMLInputElement>
React.FormEvent<HTMLFormElement>
React.KeyboardEvent<HTMLInputElement>
React.FocusEvent<HTMLInputElement>
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
Let the handler be inferred when written inline:
<input onChange={(e) => setValue(e.target.value)} />
-------------------------------------------------------------------------------
GENERIC COMPONENTS
-------------------------------------------------------------------------------
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyOf: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
return <ul>{items.map((i) => <li key={keyOf(i)}>{renderItem(i)}</li>)}</ul>;
}
In a .tsx file, a bare <T> in an arrow function is parsed as JSX. Add a
trailing comma:
const identity = <T,>(x: T): T => x;
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Typing children as JSX.Element when it should be React.ReactNode.
ReactNode covers strings, numbers, arrays, null, and fragments.
Using any for event handlers instead of the React event types.
Forgetting to annotate useState when the initial value is null or an
empty array, which infers null or never[].
================================================================================
NODE AND EXPRESS
================================================================================
DESCRIPTION
Typing Request/Response/NextFunction, middleware, and process.env.
Express types are generic over locals and params; that is how you stop
writing any.
-------------------------------------------------------------------------------
CREATE
-------------------------------------------------------------------------------
npm install -D typescript @types/node @types/express tsx
tsconfig: "module": "NodeNext", "moduleResolution": "NodeNext",
"target": "ES2022".
-------------------------------------------------------------------------------
TYPED HANDLERS
-------------------------------------------------------------------------------
import type { Request, Response, NextFunction, RequestHandler } from "express";
interface CreateUserBody { name: string; email: string }
interface UserParams { id: string }
const createUser = async (
req: Request<unknown, unknown, CreateUserBody>,
res: Response
) => {
const { name, email } = req.body;
res.status(201).json({ name, email });
};
const getUser = async (req: Request<UserParams>, res: Response) => {
const id = Number(req.params.id); // params are always strings
};
The Request generic order is
Request<Params, ResBody, ReqBody, ReqQuery>.
-------------------------------------------------------------------------------
TYPED MIDDLEWARE
-------------------------------------------------------------------------------
const requireAuth: RequestHandler = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
res.status(401).json({ error: "unauthorized" });
return;
}
req.user = verify(token);
next();
};
req.user requires the module augmentation shown in MODULES AND
DECLARATION FILES.
Note the early return with no value: a handler typed as void cannot
return res.status(...).json(...) directly, which is a very common
error under strict.
-------------------------------------------------------------------------------
ERROR MIDDLEWARE
-------------------------------------------------------------------------------
import type { ErrorRequestHandler } from "express";
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
const status = err instanceof HttpError ? err.status : 500;
res.status(status).json({ error: toError(err).message });
};
app.use(errorHandler); // must be last
-------------------------------------------------------------------------------
PROCESS AND ENV
-------------------------------------------------------------------------------
const port = Number(process.env.PORT ?? 3000);
process.env values are string | undefined. Parse and validate at
startup rather than casting at each use site.
================================================================================
RUNTIME VALIDATION
================================================================================
DESCRIPTION
TypeScript types vanish at compile time, so anything crossing a
boundary (HTTP, database, env, localStorage, user input) is unchecked.
A schema library gives you one definition that produces both the
runtime check and the static type.
-------------------------------------------------------------------------------
ZOD
-------------------------------------------------------------------------------
import { z } from "zod";
const UserSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user"]),
createdAt: z.coerce.date(),
tags: z.array(z.string()).default([])
});
type User = z.infer<typeof UserSchema>; // the type, derived
Parsing:
const user = UserSchema.parse(data); // throws ZodError
const result = UserSchema.safeParse(data); // discriminated union
if (result.success) use(result.data);
else log(result.error.issues);
-------------------------------------------------------------------------------
AT THE BOUNDARIES
-------------------------------------------------------------------------------
API response:
const res = await fetch("/api/me");
const user = UserSchema.parse(await res.json());
Environment:
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"])
});
export const env = EnvSchema.parse(process.env);
Request body middleware:
const validate = <S extends z.ZodTypeAny>(schema: S): RequestHandler =>
(req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
res.status(400).json({ errors: result.error.issues });
return;
}
req.body = result.data;
next();
};
-------------------------------------------------------------------------------
ALTERNATIVES
-------------------------------------------------------------------------------
valibot much smaller bundle, similar API
arktype very fast, syntax closer to TypeScript itself
typia compile-time generated validators, fastest at runtime
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
"How do you make sure API responses match your types" is a strong
senior-level question. The answer is that TypeScript alone cannot, and
the fix is a schema that generates both the validator and the type so
the two cannot drift.
================================================================================
TESTING
================================================================================
DESCRIPTION
Vitest/Jest with types, typed mocks, and type-level tests via
expect-type helpers. Tests should typecheck too.
-------------------------------------------------------------------------------
VITEST WITH TYPES
-------------------------------------------------------------------------------
import { describe, it, expect, vi } from "vitest";
import type { Mock } from "vitest";
const fetchUser = vi.fn<[number], Promise<User>>();
describe("UserService", () => {
it("returns a user", async () => {
const user = await service.find(1);
expect(user).toEqual<User>({ id: 1, name: "Jake", email: "x@y.z" });
});
});
-------------------------------------------------------------------------------
TYPED MOCKS
-------------------------------------------------------------------------------
const mockRepo: jest.Mocked<UserRepository> = {
findById: vi.fn(),
create: vi.fn()
};
A partial mock without fighting the type:
const mockReq = { params: { id: "1" } } as unknown as Request;
This is one of the legitimate uses of a double assertion.
-------------------------------------------------------------------------------
TESTING TYPES THEMSELVES
-------------------------------------------------------------------------------
import { expectTypeOf } from "vitest";
expectTypeOf(loadUser).returns.resolves.toEqualTypeOf<User>();
expectTypeOf<Partial<User>>().toMatchTypeOf<{ id?: number }>();
Or a hand-rolled compile-time assertion:
type Expect<T extends true> = T;
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2) ? true : false;
type _test = Expect<Equal<ReturnType<typeof add>, number>>;
================================================================================
MIGRATING FROM JAVASCRIPT
================================================================================
DESCRIPTION
allowJs, then checkJs or file-by-file rename, then strict last. Convert
leaves first. Prefer @ts-expect-error over @ts-ignore.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
1. Add TypeScript with allowJs: true and checkJs: false. Nothing
breaks; the build still works.
2. Turn on strict: false initially, then noImplicitAny only, then the
rest one flag at a time.
3. Convert leaf modules first (utilities with no dependencies), then
work up toward entry points. Types flow upward.
4. Rename .js to .ts one file at a time. Fix the errors in that file.
Commit. Repeat.
5. Use // @ts-expect-error rather than // @ts-ignore for anything you
are deferring. @ts-expect-error errors when the problem is fixed,
so it cleans itself up; @ts-ignore silently rots.
6. Type third-party gaps with minimal declare module stubs rather than
installing types you do not need.
7. Turn on strict last, file by file, then delete the any escape
hatches you added along the way.
JSDoc typing without converting files:
/**
* @param {string} name
* @param {number} [age]
* @returns {{ name: string, age: number }}
*/
function createUser(name, age = 0) { }
/** @type {import("./types").User} */
const user = load();
With checkJs: true this gives most of the benefit with zero build
changes, which is often the right first step on a large codebase.
================================================================================
COMMON COMPILER ERRORS
================================================================================
DESCRIPTION
The tsc diagnostics you will actually hit, with the usual fix next to
each one.
-------------------------------------------------------------------------------
EXAMPLES
-------------------------------------------------------------------------------
TS2322: Type 'X' is not assignable to type 'Y'
The shapes do not match. Read the nested "Types of property ... are
incompatible" lines at the bottom; that is where the real problem
is.
TS2339: Property 'x' does not exist on type 'Y'
Either a typo, or the value is a union that has not been narrowed,
or a third-party type needs augmenting.
TS2531 / TS18047: Object is possibly 'null'
strictNullChecks working correctly. Narrow with a guard, ?., ??,
or (last resort) !.
TS7006: Parameter 'x' implicitly has an 'any' type
noImplicitAny. Annotate the parameter, or give the enclosing
function a contextual type.
TS2345: Argument of type 'X' is not assignable to parameter of type 'Y'
Same as 2322 but at a call site.
TS2739 / TS2741: Type is missing properties
Usually an object literal missing required fields, or Partial<T>
being passed where T is required.
TS2367: This comparison appears unintentional
Comparing two types with no overlap, for example a narrowed literal
against a value it can never equal. Often a real bug.
TS7053: Element implicitly has an 'any' type because expression of type
'string' can't be used to index type 'X'
Indexing an object with a plain string. Fix with
key as keyof typeof obj, or add an index signature, or use a Map.
TS2551: Property 'x' does not exist. Did you mean 'y'?
Typo, and the compiler already told you the answer.
TS2589: Type instantiation is excessively deep
A recursive conditional or template literal type went too far.
Simplify, or add a depth limit parameter.
TS1205 / isolatedModules re-export errors
Re-exporting a type without the type modifier. Use
export type { X }.
TS2769: No overload matches this call
Read each listed overload; usually one argument is a near miss,
often string vs a literal union.
================================================================================
BEST PRACTICES
================================================================================
DESCRIPTION
Naming, what to annotate, what to avoid, and the TypeScript interview
questions that recycle this file.
-------------------------------------------------------------------------------
NAMING CONVENTIONS
-------------------------------------------------------------------------------
PascalCase types, interfaces, classes, enums, type parameters
camelCase variables, functions, properties, methods
UPPER_SNAKE true constants
T, K, V, E conventional single-letter type parameters
TItem, TResult descriptive type parameters when there are several
Do NOT prefix interfaces with I. The C# IFoo convention is not used in
TypeScript, because interface and type are interchangeable for most
shapes and the prefix leaks an implementation detail.
Avoid the Impl suffix on classes for the same reason.
-------------------------------------------------------------------------------
TYPING STYLE
-------------------------------------------------------------------------------
Annotate function parameters and exported function return types.
Let local variable types be inferred.
Prefer unknown over any at every boundary.
Prefer discriminated unions over optional-property grab bags.
Prefer type-level derivation (typeof, keyof, as const) over duplicating
a shape in both a type and a runtime constant.
Prefer satisfies over an annotation for constant data.
Prefer readonly on function parameters you do not mutate.
Use branded types when two same-shaped values must not be mixed
(UserId vs PostId, Celsius vs Fahrenheit).
-------------------------------------------------------------------------------
WHAT TO AVOID
-------------------------------------------------------------------------------
any as a habit. Each one is a hole the compiler will not check.
Type assertions where a guard would do.
Non-null ! scattered through a file; it usually means the type is wrong
upstream.
Enums, for the runtime-emit reasons in that section.
namespace, which predates ES modules and should not be used in new code.
Clever conditional types in application code. Library code earns them;
business logic rarely does.
Types that duplicate a schema. Derive one from the other.
-------------------------------------------------------------------------------
COMMON INTERVIEW QUESTIONS
-------------------------------------------------------------------------------
Difference between interface and type, and when each is required.
Difference between any, unknown, and never.
What does strictNullChecks change.
Explain structural typing and how it differs from nominal typing.
Explain generics and constraints, with a concrete example.
What is a discriminated union and why is it useful.
How do you achieve exhaustiveness checking.
What are keyof, typeof, and indexed access types.
Explain Partial, Pick, Omit, Record, and how you would implement Pick.
Why do types not exist at runtime, and what do you do about it.
What does satisfies do that an annotation does not.
How do you type an API response safely.
-------------------------------------------------------------------------------
COMMON MISTAKES
-------------------------------------------------------------------------------
Treating TypeScript as C# with different syntax. Structural typing,
erased generics, and discriminated unions instead of class hierarchies
are the three habits that need unlearning.
Trusting the type of anything that crossed a network or a parse
boundary. The compiler believes whatever you asserted; the data does
not care.
Fighting an error with an assertion instead of reading it. Most
TypeScript errors are correct, and the last few lines of a long error
message usually name the exact incompatible property.
-------------------------------------------------------------------------------
INTERVIEW NOTES
-------------------------------------------------------------------------------
The strongest signal in a TypeScript interview is knowing the limits:
that types are erased, that the system is intentionally unsound in
specific places, and that runtime validation is a separate job. Anyone
can recite Partial and Pick.
================================================================================
END OF FILE
================================================================================