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.
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.
.ts files and proves every value matches its declared shape. Mismatches are errors you see while editing..js that browsers and Node can run.The checker proves your types up front; the emitted JavaScript carries none of them. Remember that gap — Part 7 is about where it bites.
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.
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.
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.
Anything carrying x and y is a Point — extra fields are fine; a missing one is not.
string, number, boolean, null, undefined, plus bigint and symbol.?.number[] for a list, [string, number] for a fixed pair.Annotate a variable with :, or let TypeScript infer it from the value — usually let it infer.
interface and type both name a shape; prefer one and stay consistent. ? marks an optional field.
A homogeneous list vs a fixed-length, positional tuple — order and arity matter.
Parameters and the return type form the contract; callers and the body are both checked against it.
interface vs type — which to reach fortypethe moment you need anything that isn't a simple object.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.
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.Outside the guard xis the whole union; inside each branch it is one specific type, with that type's methods.
typeof — for primitives: typeof x === "string".instanceof — for classes: err instanceof Error.in — for objects: "radius" in shape picks the member with that field.x is Cat teaches the compiler your own rule.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.
One tag field routes each branch to exactly one shape — and powers exhaustiveness checking.
neverAdd 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.
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.
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.Pass a string[] and the result is a string — not any. The type the caller supplied flows from input to output.
<T> after the name declares the parameter; T is just a conventional letter.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.The constraint admits anything with a length — strings and arrays in, bare numbers out.
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.
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.
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.Partial walks every key of User and adds ? — one transform, applied uniformly.
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.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.
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.
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.
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.
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": 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 sits above every type (and forces a check); never sits below them all; any steps outside the system entirely.
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.as, !) override the checker — use them rarely and deliberately.any lets bad data travel until it crashes; unknown stops it at the door until you check.
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.
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.
.d.ts types consumers rely on.tsc --noEmit even when a bundler produces the JS.A Go-based bundler/transpiler that strips types in milliseconds. It never looks at the types — it just deletes them.
tsc --noEmit.A Rust-based transform that powers tools like Next.js and fast test runners. Like esbuild, it transpiles per-file and erases types.
tsc for the checking half.@babel/preset-typescript strips types inside an existing Babel pipeline — handy when you are already on Babel.
const enumand namespaces) don't work.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.
JSON.parse arrives as any/unknown; a bare as User just assertsa shape no one verified. Parse it, don't assume it.Validate once at the boundary; everything inside that line gets to trust its types.
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.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.
A fluent schema builder; z.infer derives the static type, and the ecosystem (forms, RPC, ORMs) is huge.
Validators are standalone functions you compose with a pipe, so bundlers tree-shake away everything you don't import.
Codecs built on fp-ts; decoding returns an Either of errors or a typed value, with strong composability.
fp-ts learning curve; less active than the newer options.fp-ts who want functional rigor end-to-end.any.Keep the caller's type flowing through; derive types with utilities instead of duplicating them.tsc. Strict mode plus a real type-check step — transpilers alone never check.Five quick questions on structural typing, narrowing, generics, strict mode, and where types lie — instant feedback, no sign-in.
Navigate with ← → or scroll · back to library