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 tolerating any navigation 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 fetch wrappers 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.