Advanced JavaScript & TypeScript¶
The bar: you can read and write modern async JavaScript without flinching, and you can add TypeScript types that catch bugs at compile time instead of in production.
You already know the fundamentals: variables, loops, functions, the DOM. That gets you a working script. It does not get you production software. The gap between the two is filled by modern language features, disciplined async handling, and a type system that stops mistakes before they ship.
This module closes that gap. Every pattern here shows up in the Vets Who Code app you'll be committing to, and every one of them is the difference between code a teammate trusts and code they have to babysit. You are learning the vocabulary of a professional codebase.
9.1 ES6+ Features¶
Objective: use modern JavaScript syntax fluently and know why each feature exists.
The 2015 language revision (ES6) and everything after it replaced a decade of workarounds with real syntax. let and const gave us block scoping, so a variable declared inside an if stays inside that if — no more the accidental leaks that plagued var. Reach for const by default and let only when you truly reassign. You almost never need var again.
Arrow functions are shorter, but the real reason they matter is this binding: an arrow function inherits this from where it was defined, not from how it's called. That single behavior kills the most common bug in old event-handler code. The trade-off is that arrow functions can't be used as methods that need their own this or as constructors — know when not to use them.
The rest — template literals, destructuring, spread and rest, default parameters, enhanced object literals — are about saying more with less and making intent obvious. Destructuring a function's arguments tells the reader exactly what that function consumes. Spread makes copying without mutation a one-liner. The gotcha self-taught engineers hit: spread is a shallow copy. Nested objects still share references, and that surprise mutation will bite you.
- Drill: rewrite a
varloop usingletand observe the scoping difference. - Drill: convert three callback functions to arrow functions; find one where the arrow breaks
this. - Drill: destructure nested objects and arrays with defaults, e.g.
const { user: { name = "guest" } = {} } = data. - Drill: use
...spreadto merge objects and...restto collect variadic arguments; prove the shallow-copy gotcha. - Drill: replace string concatenation with template literals, then write one tagged template.
9.2 Advanced Async Patterns¶
Objective: orchestrate complex asynchronous work correctly and predictably.
async/await made async code read like sync code, but reading easy hides real decisions. The biggest one: concurrent versus sequential. Awaiting three independent calls one after another is three times slower than firing them together with Promise.all. Learn to see when operations don't depend on each other and run them at once.
Know the whole Promise combinator family. Promise.all fails fast if any promise rejects. Promise.allSettled waits for every result, success or failure — use it when one bad call shouldn't sink the batch. Promise.race resolves on the first to finish, the basis of timeouts. Picking the wrong one is a subtle, expensive bug.
Async iterators and generators let you stream data lazily instead of loading everything into memory. And async error handling is where beginners bleed: a rejected promise with no try/catch or .catch() becomes an unhandled rejection that can crash a Node process. Cancellation is the last mile — an AbortController lets you cancel a fetch when a user navigates away, so you're not setting state on a component that's already gone.
- Drill: convert a sequential
awaitchain toPromise.alland measure the speedup. - Drill: handle a partial-failure batch with
Promise.allSettled; branch onstatus. - Drill: build a timeout using
Promise.race. - Drill: write an async generator with
for await...of. - Drill: cancel an in-flight
fetchwithAbortControllerand asignal.
9.3 Modern JavaScript Patterns¶
Objective: structure code with the patterns professional teams rely on.
Modules are how real codebases stay sane. import/export split code into files with clear boundaries, and dynamic import() lets you load code only when it's needed — the foundation of code-splitting that keeps an app's initial bundle small. In a Next.js app that directly affects how fast a page loads for a veteran on a bad connection.
Classes and inheritance give you a familiar object model, but modern JavaScript leans hard on functions. Higher-order functions — functions that take or return other functions — are the engine behind map, filter, and reduce, and behind React itself. Functional patterns favor small, composable, predictable pieces over sprawling stateful objects.
Immutability ties it together. Instead of mutating data in place, you produce a new copy with the change applied. React's rendering depends on this: it detects change by comparing references, so mutating state directly means updates that silently don't render. The mistake to avoid is reaching for a class hierarchy when a couple of pure functions would do — inheritance is rarely the simplest answer.
- Drill: split a file into named and default exports; import both ways.
- Drill: lazy-load a module with dynamic
import(). - Drill: chain
map,filter, andreduceto transform a dataset without aforloop. - Drill: write a higher-order function that returns a configured function (a closure).
- Drill: update nested state immutably using spread instead of mutation.
9.4 TypeScript Fundamentals¶
Objective: add type safety so the compiler catches mistakes before runtime.
TypeScript is JavaScript with a type layer checked at compile time and erased before it runs. The payoff is enormous: a typo in a property name, a function called with the wrong argument, a value that might be undefined — all caught while you type instead of in a user's browser. Start with annotations on primitives, arrays, and object shapes, then let inference do the rest; you don't annotate what TypeScript can already figure out.
interface versus type trips people up. Both describe object shapes. Interfaces are open to extension and read cleanly for public contracts; type aliases handle unions, intersections, and anything an interface can't express. Pick one style and stay consistent. Union types (string | number) model "one of these," and intersection types combine shapes.
Generics are the leap that unlocks reusable, type-safe code — a function that works over any type while preserving it, like function first<T>(arr: T[]): T. Constraints (<T extends { id: string }>) narrow what's allowed. Round it out with type narrowing and guards, which let the compiler follow your if checks, and the built-in utility types — Partial, Required, Pick, Omit — that reshape existing types instead of retyping them by hand.
- Drill: annotate primitives, arrays, and an object shape; then delete the annotations TypeScript can infer.
- Drill: model the same shape as an
interfaceand atype; extend both. - Drill: write a generic function with a constraint (
<T extends ...>). - Drill: narrow a
string | numberunion withtypeofguards. - Drill: derive new types from one source using
Pick,Omit, andPartial.
9.5 Advanced TypeScript¶
Objective: use the type system's power features to model hard cases precisely.
Once the fundamentals are muscle memory, the advanced features let the type system compute types from other types. Conditional types (T extends U ? X : Y) branch on type relationships. Mapped types transform every property of a type at once — the machinery behind the utility types you already use. Template literal types build string types from other types, useful for typed event names or route strings.
Inference and typeof let you pull a type out of runtime code — type Config = typeof defaultConfig keeps a type in lockstep with the value it describes, so they can never drift apart. This is how you avoid maintaining a type and a value as two separate sources of truth.
Declaration files (.d.ts) and the @types packages let TypeScript understand libraries written in plain JavaScript. And tsconfig.json is the control panel for the whole checker. The single most important setting is "strict": true — turn it on and leave it on. Loosening strictness to make a red squiggle disappear is trading a five-minute fix now for a production bug later.
- Drill: write a conditional type that unwraps an array element type.
- Drill: build a mapped type that makes every property
readonly. - Drill: construct a template literal type like
`on${Capitalize<Event>}`. - Drill: derive a type from a value with
typeofand index access. - Drill: install an
@types/*package and enablestrictintsconfig.json.
9.6 Error Handling and Debugging¶
Objective: handle failure gracefully and debug efficiently in dev and production.
Errors are not exceptions to the job — they are the job. try/catch/finally is the base tool, with finally for cleanup that must run either way. But swallowing an error in an empty catch block is how bugs go silent; catch only what you can actually handle, and let the rest propagate. Custom error classes (class NotFoundError extends Error) let calling code branch on what went wrong instead of parsing message strings.
In React, an error in one component shouldn't blank the whole page. Error boundaries catch render-time errors in a subtree and show a fallback UI, containing the blast radius. Wire them around the risky parts of your interface.
Debugging is a skill you build deliberately. The browser DevTools are more than console.log: breakpoints, the call stack, watch expressions, and the network panel will find a bug in minutes that logging takes an hour to corner. Source maps connect your minified production bundle back to the original TypeScript, so a stack trace points at the line you wrote instead of bundle.min.js:1:48210. Learn to read both.
- Drill: write
try/catch/finallywherefinallyruns on both success and failure. - Drill: define a custom error class and branch on
instanceofin the catch. - Drill: add a React error boundary around a component that throws.
- Drill: set a DevTools breakpoint, step through, and inspect the call stack.
- Drill: confirm source maps map a production error back to your source line.
Hands-On Lab: TypeScript Utilities¶
Objective: ship a small library of strongly-typed, tested utility functions.
What you ship
A reusable TypeScript utilities package that proves you can wield the type system, not just satisfy it. Deliverables:
- Type-safe transformers — data transformation functions (grouping, mapping, picking fields) whose input and output types are fully inferred, no
any. - Custom type guards — user-defined guard functions (
value is Foo) that narrow unknown data safely at runtime boundaries. - Generic data structures — at least one generic container (stack, queue, or typed map) that preserves its element type across every operation.
- Type tests — comprehensive tests that assert both runtime behavior and type correctness, so a future change that breaks a type fails the suite instead of shipping.
On the VWC codebase
Everything here is live in the Vets Who Code app: it runs TypeScript 5 in strict mode, and the same async, immutability, and typing discipline applies whether you're writing a src/pages/api handler or a component. When you add types, install declarations with npm (yarn/pnpm/bun are blocked), and let Biome — not ESLint or Prettier — handle lint and format. Type tests run under Jest with React Testing Library, so the utilities you build in this lab are shaped exactly like the code you'll commit to production.