TypeScript in Frameworks
Frameworks are where types earn their keep daily — and where the most type-system abuse happens. Every major framework is TypeScript-first in 2026, but each integrates the checker differently. This lesson shows the shared principles, the framework-specific idioms, and the honest limits of each integration.
Principles that transcend frameworks
Before the specifics, four rules that apply to React, Vue, Svelte, and beyond:
Type props as data — model the impossible states away
Purpose of the example: show the single highest-value frontend typing decision: making invalid component states unrepresentable.
// JS habit: flags that allow nonsense combinations
// { isLoading: boolean; data: Data | null; error: Error | null }
// → isLoading + data + error all set: which renders?
// TS approach: a discriminated union — exactly one state exists at a time
type AsyncView<Data> =
| { status: "loading" }
| { status: "success"; data: Data } // data only exists on success
| { status: "error"; error: Error }; // error only exists on failure
function AsyncViewComponent<Data>({ view }: { view: AsyncView<Data> }) {
switch (view.status) {
case "loading": return Spinner();
case "success": return Content(view.data); // view.data: Data — no !
case "error": return Alert(view.error); // narrowing again
}
}
What you should see: the component body has no null checks and no "is loading but has data?" branches — they cannot compile. This is the types lesson's discriminated union applied to UI state, and it is the same idiom in every framework.
Type boundaries: events, props, and slots
Purpose of the example: the React idiom for events, where the default types are traps.
// antipattern: `event: any` — accepted everywhere, checks nothing
// function SearchBox({ onChange }: { onChange: (e: any) => void })
import type { ChangeEvent } from "react";
function SearchBox({ onChange }: {
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}) {
// e.target is typed as HTMLInputElement → .value: string — no casting:
return <input onChange={(e) => onChange(e)} />;
}
// Generic list props: RenderProps pattern with generics (from the generics lesson)
function List<T>({ items, renderItem }: {
items: T[];
renderItem: (item: T) => React.ReactNode;
}) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
What you should see: full inference on event objects and generic list contents. What we learn: framework types are libraries of precisely the patterns from Phases 2–3 — unions, generics, narrowing — applied to component APIs.
Framework-specific idioms
Same principles, different mechanics. The examples below are the canonical typing idiom for each of the three major frameworks.
Vue: script setup and defineProps
Purpose of the example: type-safe props and emits without runtime schemas — the compiler reads the types.
<script setup lang="ts">
// Type-only declaration: defineProps infers the prop types at compile time.
// `id?` and the union type flow into the template with full narrowing:
defineProps<{ id: string; status: "active" | "archived"; note?: string }>();
// Typed emits — the event contract, not string folklore:
const emit = defineEmits<{ (e: "save", id: string): void;
(e: "cancel"): void }>();
</script>
What you should see: mis-typed props and unknown events fail at compile time, in the component that declares them. Vue's language tools compile the single-file component through the TypeScript checker, so the template's bindings are checked too.
Svelte: lang="ts" and typed stores
Purpose of the example: a typed store — the shared-state primitive — with the same union discipline as props.
<script lang="ts">
import { writable, derived } from "svelte/store";
// The store's type is the union from earlier — consumers cannot
// construct invalid states:
export const view = writable<AsyncView<Cart>>({ status: "loading" });
// derived stores keep the type flowing through transformations:
export const itemCount = derived(view, (v) =>
v.status === "success" ? v.data.items.length : 0);
</script>
What you should see: itemCount is a Store<number>; subscribing in the template is fully checked. Svelte 5's runes ($props(), $state()) are similarly type-driven.
Where framework typing hits its limits
Gaps you will meet
Know the gaps so they do not surprise you in review:
- Stringly-typed routes. Framework routers (React Router's path strings, route names in Vue Router) historically accept
to="/usres/42"— a typo, unchecked. Type-safe route layers (typed links, code-generated route types) exist; adopt one rather than toleratinganynavigation helpers. - CSS and template string references are untyped by design — class names, element IDs.
- Two-way bindings in Vue/Svelte hide reassignment from the checker; the value's type is checked, the binding lifecycle is not.
- Server data is untyped at the network boundary in all frameworks — the errors lesson's validation discipline applies identically whether you use React Query, Loaders, or Actions. End-to-end typed stacks (tRPC, server-side
fetchwrappers with schema types) exist precisely to close this gap.
The rule that transfers
What we learn: framework types cover the component contract, not the system. The typed boundaries you design — state unions, event payloads, domain types in shared modules — carry across every framework you will ever use.
Practice: one feature, typed end to end
Implement the AsyncView in your framework
Build a small "notifications" feature: fetch a list, render three states (loading / list / error), allow dismissing an item. Requirements: the view state is a discriminated union; every event handler's parameter is a framework type (no any); domain types live in a separate module the component imports. What you should see: the component compiles with zero casts — and deleting the "loading" branch of the union is a compile error, not a UI bug.
Audit for impossible states
Find one component in a real project with isLoading-style boolean flags plus nullable data. Replace them with the union from this lesson and count the branches that disappear — including the ones that were hiding real bugs (for example, "loading and error both true"). What we learn: refactors driven by types regularly delete defensive code that was only necessary because the types allowed nonsense.