Library
00/07 · ~32 min
GUIDEDECK · catching bugs before they ship

TypeScript — types that
document and defend
your code.

A 32-minute working session on the type system behind modern JavaScript — from why types pay off, through unions, generics and utility types, to strict mode and the places where types quietly lie.

~32 MINBEGINNER → INTERMEDIATETYPESCRIPT 5+
SCROLL
01 · Why types 3 min

Catch whole classes of bugs
before the code ever runs.

JavaScript only tells you something is wrong when a user hits it in production. TypeScript moves that feedback to the moment you type — it reads your code, checks every value against the shape you promised, and flags the mismatch in your editor. The same notation doubles as always-accurate documentation.

TypeScript a typed superset of JavaScript — is plain JS plus an optional static type layer. A separate program checks that layer before your code runs, then erases it, emitting ordinary JavaScript. Nothing about types exists at runtime — they are a tool for you and your editor, not the engine.

Two jobs, two tools

  • Type-check — a checker reads .ts files and proves every value matches its declared shape. Mismatches are errors you see while editing.
  • Transpile — the same source is stripped of types and turned into .js that browsers and Node can run.
  • Crucially, those can be different tools — and only the checker actually understands types (Part 6).
app.ts source + types tsc type-check errors here ✕ app.js types erased runtime sees NO types

The checker proves your types up front; the emitted JavaScript carries none of them. Remember that gap — Part 7 is about where it bites.

What you get for the effort

0

typos at runtime. user.naem is an error in your editor, not a crash in production.

fearless refactors. Rename a field and the checker lists every call site that needs updating.

📖

living docs. A type signature explains what a function wants and returns — and can never drift out of date.

real autocomplete. The editor knows the shape, so it suggests the right fields and catches the wrong ones.

JavaScript — fails in production
function total(cart) { return cart.reduce((sum, i) => sum + i.prce, 0) } // typo "prce" → every line adds undefined → NaN // nothing complains until a user checks out
TypeScript — caught while typing
type Item = { price: number } function total(cart: Item[]): number { return cart.reduce((sum, i) => sum + i.prce, 0) } // ✕ Property 'prce' does not exist on 'Item'

You already meet TypeScript everywhere here — it underpins Modern React components and the stores in State Management, and it is the natural language for the design ideas in OOP & Architecture. This deck is the type system those decks lean on.

02 · The basics 5 min

TypeScript judges values by their
shape, not their name.

The first surprise for newcomers: types aren't matched by where they came from, but by what they look like. If a value has the fields a function needs, it fits — no inheritance or label required. That is structural typing, and it makes the system feel light and JavaScript-native.

Structural typing — also called duck typing, checked at compile time — means two types are compatible when their shapes match. A value is assignable to a type if it has at least the required properties, with compatible types. The name of the class or interface is irrelevant; only the structure counts.
needs Point { x: number, y: number } { x, y } fits ✓ { x, y, z } extra field still fits ✓ { x } missing y ✕

Anything carrying x and y is a Point — extra fields are fine; a missing one is not.

The building blocks

  • Primitivesstring, number, boolean, null, undefined, plus bigint and symbol.
  • Objects — a record of named fields, each with its own type; optional fields are marked ?.
  • Arrays & tuplesnumber[] for a list, [string, number] for a fixed pair.
  • Functions — typed by their parameters and return value.
primitives

The atoms

Annotate a variable with :, or let TypeScript infer it from the value — usually let it infer.

let name: string = "Ada" let age = 42 // inferred: number let on: boolean = true
objects

Named shapes

interface and type both name a shape; prefer one and stay consistent. ? marks an optional field.

interface User { id: number name: string bio?: string // optional }
arrays

Lists & tuples

A homogeneous list vs a fixed-length, positional tuple — order and arity matter.

const ids: number[] = [1, 2] const pair: [string, number] = ["a", 1]
functions

Typed contracts

Parameters and the return type form the contract; callers and the body are both checked against it.

function greet( n: string ): string { return `Hi ${n}` }

interface vs type — which to reach for

  • interface describes object shapes and can be extended and merged — a good default for public object contracts and class shapes.
  • type is an alias for any type — unions, intersections, tuples, mapped and conditional types (the next parts) all need it.
  • They overlap heavily for plain objects. Pick a house style; reach for typethe moment you need anything that isn't a simple object.
03 · Unions, intersections & narrowing 5 min

Model "this or that" — then let the
compiler prove which one you have.

Real values are often one of several shapes: a request is loading or loaded or failed. A union captures that exactly. The payoff is narrowing: inside an if or a switch, TypeScript tracks what you have checked and lets you safely use the matching members.

Union (A | B) — a value that is one of several types. Intersection (A & B) — a value that satisfies all of several types at once (it merges their fields). Narrowing is how a runtime check (like typeof) tells the compiler which member of a union you currently hold.
string | number union typeof x guard x: string .toUpperCase() ✓ x: number .toFixed(2) ✓

Outside the guard xis the whole union; inside each branch it is one specific type, with that type's methods.

The four ways to narrow

  • typeof — for primitives: typeof x === "string".
  • instanceof — for classes: err instanceof Error.
  • in — for objects: "radius" in shape picks the member with that field.
  • Custom guard — a function returning x is Cat teaches the compiler your own rule.

Discriminated unions — the workhorse pattern

Give every member of a union a shared literal tag field. A switch on that tag narrows perfectly, and the compiler can prove you handled every case.

type Shape = | { kind: "circle"; r: number } | { kind: "square"; size: number } function area(s: Shape): number { switch (s.kind) { // the tag case "circle": return Math.PI * s.r ** 2 case "square": return s.size ** 2 } } // add a kind → compiler flags missing case
switch(kind) kind: "circle" has r kind: "square" has size

One tag field routes each branch to exactly one shape — and powers exhaustiveness checking.

Exhaustiveness with never

Add a default that assigns the value to never. If you later add a union member and forget to handle it, the assignment fails to compile — a free reminder.

default: { const _exhaustive: never = s // ✕ if a new kind is unhandled return _exhaustive }
04 · Generics 5 min

Write it once, keep the type
flowing all the way through.

A generic is a type parameter — a placeholder, written <T>, that the caller fills in. It lets one function or type work over any type while still remembering exactly which one was used. The alternative, any, "works" too — but it throws that knowledge away.

Generic a type with a parameter you fill in later. Array<T> is the everyday example: a T[] is "an array of sometype", and once you make a string[] the compiler knows every element is a string. You write the abstraction once; the concrete type rides along.
["a","b"] string[] first<T> (arr: T[]) => T "a" string ✓ T = string, locked in by the call

Pass a string[] and the result is a string — not any. The type the caller supplied flows from input to output.

Reading the syntax

  • <T> after the name declares the parameter; T is just a conventional letter.
  • You rarely pass T explicitly — TypeScript infers it from the arguments you give.
  • <T extends …> constrains what T can be, so you can safely use the guaranteed fields.
  • <T = string> gives a default when none can be inferred.
// preserves the element type for every caller function first<T>(arr: T[]): T | undefined { return arr[0] } const n = first([1, 2, 3]) // number | undefined const s = first(["a", "b"]) // string | undefined // a constraint lets you use a guaranteed field function longest<T extends { length: number }>(a: T, b: T) { return a.length >= b.length ? a : b // .length is safe }
T extends { length: number } string ✓ any[] ✓ number ✕

The constraint admits anything with a length strings and arrays in, bare numbers out.

any — throws the type away
function wrap(x: any): any { return [x] } const r = wrap(42) // r is any r.toExponential() // no error, no help — could crash
generic — keeps the type
function wrap<T>(x: T): T[] { return [x] } const r = wrap(42) // r is number[] r[0].toExponential() // ✓ checked, with autocomplete

Generics are how Promise<T>, Map<K, V>and your favourite data structures stay type-safe. Reach for one whenever a function's output type depends on its input type.

05 · Utility, mapped & conditional types 5 min

Build new types from old ones —
without retyping a thing.

Types compose. From one User shape you can derive a "patch" with every field optional, a read-only copy, or a lookup keyed by id — all kept in sync with the original. TypeScript ships a toolbox of these transforms, and they are built from mapped and conditional types you can write yourself.

Utility types built-in generics that transform a type — like Partial<T>, Pick<T, K> and Record<K, V>. Each is just a mapped typeunder the hood: a loop over the keys of T that rewrites each property. Derive types instead of duplicating them — change the source once and every derived type follows.
User id: number name: string Partial map keys Partial<User> id?: number name?: string

Partial walks every key of User and adds ? — one transform, applied uniformly.

The ones you'll use weekly

  • Partial<T> / Required<T> — make every field optional / mandatory.
  • Pick<T, K> / Omit<T, K> — keep / drop a subset of keys.
  • Record<K, V> — an object with keys K and values V.
  • Readonly<T>, ReturnType<F>, Awaited<P> — and more.
U
Utility types
Derive a related type instead of writing a new one.
+

Keep one source of truth. A form edits a draft, so it wants every field optional; a lookup table wants a Record. Derive both from User and they can never drift.

interface User { id: number; name: string; email: string } type Draft = Partial<User> // all optional type Card = Pick<User, "id" | "name"> // subset type ById = Record<number, User> // lookup map type Public = Omit<User, "email"> // drop a field
M
Mapped types
Loop over the keys of a type and rewrite each one.
+

A mapped type is a for-loop for keys: [K in keyof T] visits each property so you can change its modifier or type. Every utility type above is one of these.

your own Partial
type MyPartial<T> = { [K in keyof T]?: T[K] } // "?" added to every key
a read-only stamp
type Frozen<T> = { readonly [K in keyof T]: T[K] } // readonly added everywhere
C
Conditional types
A type that branches — and can extract with infer.
+

T extends U ? X : Y is an if for types. Pair it with infer to pull a type out of another — exactly how ReturnType and Awaited work.

// unwrap the element type of an array type ElementOf<T> = T extends (infer U)[] ? U : T type A = ElementOf<string[]> // string type B = ElementOf<number> // number (no array → itself)

Powerful, but easy to overdo. If a conditional type takes a colleague ten minutes to read, a plainer explicit type is often the kinder choice.

06 · tsconfig & strict mode 5 min

Turn strict on — and learn the
difference between any, unknown & never.

How hard TypeScript pushes back is a setting. strict mode is the bundle of checks that make the type system actually trustworthy — above all, taking null and undefined seriously. And three special types decide how much safety you keep: any opts out, unknown stays safe, nevermeans "can't happen".

Strict mode the "strict": true flag in tsconfig.json — switches on a family of checks at once, including strictNullChecks (null and undefined are no longer silently part of every type) and noImplicitAny (an un-inferable type is an error, not a free any). Start new projects strict; it is far harder to add later.
unknown — accepts anything safe: must narrow before use string · number · User … the real types you use never — accepts nothing empty / impossible any checks OFF escapes both

unknown sits above every type (and forces a check); never sits below them all; any steps outside the system entirely.

Prefer unknown to any

  • any — disables checking for that value. One any can quietly infect everything it touches.
  • unknown — "could be anything, so prove what it is first." The safe landing spot for outside data.
  • never — a value that can't exist: a function that always throws, or an exhausted union.
  • Assertions (as, !) override the checker — use them rarely and deliberately.
// any — the checker stops helping let a: any = JSON.parse(body) a.user.name.toUpperCase() // compiles… may explode // unknown — you must prove the shape first let u: unknown = JSON.parse(body) u.user // ✕ 'u' is of type 'unknown' if (typeof u === "object" && u !== null) { // now safely narrow further… }
any spreads risk → crash later ✕ unknown guard → safe to use ✓

any lets bad data travel until it crashes; unknown stops it at the door until you check.

The toolchain — who checks, who only transpiles

A vital, often-missed fact: most build tools never check types. They strip them for speed. Only tsc actually understands the type layer — so even a turbo-fast build still needs a separate type-check step.

tsc — the official type-checker & emitter

The reference compiler. It is the only tool here that actually proves your types, and the one that emits .d.ts declaration files for libraries.

Pro
Authoritative type-checking; generates the .d.ts types consumers rely on.
Con
Slowest option on large repos (a native Go port is in preview to close that gap).
How to choose
Your CI type gate and library builds — run tsc --noEmit even when a bundler produces the JS.

esbuild — transpile-only, very fast

A Go-based bundler/transpiler that strips types in milliseconds. It never looks at the types — it just deletes them.

Pro
Extremely fast builds and dev startup; tiny config.
Con
No type-checking at all — a type error sails straight through.
How to choose
App builds where speed matters; always pair it with a separate tsc --noEmit.

swc — Rust transpiler behind many frameworks

A Rust-based transform that powers tools like Next.js and fast test runners. Like esbuild, it transpiles per-file and erases types.

Pro
Fast, framework-integrated; often already wired up for you.
Con
Transpile-only — no type-checking.
How to choose
When your framework already uses it; keep tsc for the checking half.

Babel — TypeScript via a preset

@babel/preset-typescript strips types inside an existing Babel pipeline — handy when you are already on Babel.

Pro
Fits codebases already built around Babel and its plugins.
Con
No checking, per-file transpile, and a few TS features (like const enumand namespaces) don't work.
How to choose
Only if you're already invested in Babel — otherwise prefer esbuild/swc.
07 · Patterns & pitfalls — where types lie 4 min

Types vanish at runtime — so guard
the edges where data comes in.

A type annotation is a promise you make to the compiler, not a runtime check. JSON.parse, fetch, form bodies and as assertions can all hand you a value whose real shape differs from its declared type. The fix is simple: validate untyped data at the boundary, then trust your types inside.

Where types lie at every boundary with the outside world. Inside your code the compiler keeps everything honest. But data entering from a network, a database, the URL or JSON.parse arrives as any/unknown; a bare as User just assertsa shape no one verified. Parse it, don't assume it.
network / JSON unknown validate schema.parse typed core trust types ✓ bad shape → throw ✕ one checkpoint at the edge

Validate once at the boundary; everything inside that line gets to trust its types.

Two everyday safety habits

  • as const — freezes a literal to its narrowest type ("circle" stays "circle", not string) — ideal for config and union tags.
  • satisfies — checks a value against a type without widening it, so you keep both the guarantee and the precise inferred type.
const routes = { home: "/", docs: "/docs", } satisfies Record<string, string> routes.home // still exactly "/" — not widened
as — an unchecked promise
const res = await fetch(url) const user = await res.json() as User // "as" verifies NOTHING — if the API changed, // user.name is undefined and you crash downstream
parse — verified at the edge
const User = z.object({ id: z.number(), name: z.string() }) const res = await fetch(url) const user = User.parse(await res.json()) // throws on bad data; user is typed AND real

Runtime validation libraries — parse, don't trust

These libraries let you write a schema once and infer the TypeScript type from it — one source of truth that covers both compile-time and runtime. Pick by your priorities.

Zod — the ergonomic default

A fluent schema builder; z.infer derives the static type, and the ecosystem (forms, RPC, ORMs) is huge.

Pro
Great DX, type inference, and the broadest ecosystem and integrations.
Con
Larger bundle and more runtime cost than the leaner options.
How to choose
The sensible default for most apps and APIs unless bundle size is critical.

Valibot — modular & tiny

Validators are standalone functions you compose with a pipe, so bundlers tree-shake away everything you don't import.

Pro
Very small bundles — you pay only for the validators you use.
Con
Younger ecosystem; composing pipes is a touch more verbose.
How to choose
Bundle-sensitive code — edge functions, client-side, mobile web.

io-ts — functional & principled

Codecs built on fp-ts; decoding returns an Either of errors or a typed value, with strong composability.

Pro
Rigorous, highly composable codecs that fit a functional codebase.
Con
Steep fp-ts learning curve; less active than the newer options.
How to choose
Teams already using fp-ts who want functional rigor end-to-end.
  • Data that never leaves your typed code (internal function calls) needs no runtime validation — the compiler already covers it.
  • A tiny script or trusted internal payload? A hand-written type-guard function is fine — don't add a dependency for one shape.
  • Reach for a validation library at realboundaries: external APIs, user input, webhooks, config files, anything you didn't produce.
1Let inference work. Annotate boundaries — function signatures, exports — and let TypeScript figure out the rest.
2Model the domain with unions. Discriminated unions plus narrowing make impossible states unrepresentable.
3Reach for generics, not any.Keep the caller's type flowing through; derive types with utilities instead of duplicating them.
4Run strict, and run tsc. Strict mode plus a real type-check step — transpilers alone never check.
5Validate at the edges. Types vanish at runtime; parse untyped data before you trust it.
Knowledge check

Did it stick?

Five quick questions on structural typing, narrowing, generics, strict mode, and where types lie — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library