Learn / TypeScript and React Product Systems
TypeScript Component Foundations
Use props, unions, and small component contracts to make UI states explicit before screens grow complex.
Course: TypeScript and React Product Systems. Level: Intermediate. Topic: Frontend systems.
Stage: basic - Basic UI contracts - Typed React component contracts. Start with data shapes and UI boundaries that make a feature reviewable before state and effects spread.
Outcomes
- Type component props clearly.
- Represent loading, empty, error, and ready states.
- Avoid accidental any at API boundaries.
- Distinguish aliases, interfaces, literal types, tuples, and readonly arrays.
- Use discriminated unions to encode valid application states.
- Recognize when structural compatibility helps and when it hides a domain concept.
- Apply typeof, in, instanceof, equality checks, and custom predicates.
- Write assertion functions for validated boundaries.
- Use never to catch unhandled union members.
- Identify trust boundaries that require runtime validation.
- Use parser results to produce typed domain values.
- Keep validation errors useful for callers and logs.
Concepts
- props contract
- discriminated union
- unknown JSON
- component composition
- Type System Foundations
- Structural typing
- Invalid states should be unrepresentable
- TypeScript Advanced foundations
- Guided practice
- Narrowing and Control Flow
- Control-flow analysis
- Exhaustive checks
- Runtime Validation
- Parse, do not pretend
- Validation belongs at the edge
Concept flow
Show how typed react component contracts moves from trigger to implementation outcome in TypeScript React.
- API data
- Parser
- Remote state
- Component props
- Rendered UI
Session flow
- Model props contract (concept, 8 min) — Name the decisions behind props contract before writing code.
- Type component props clearly.
- Explain where props contract belongs in learning analytics dashboard.
- Build the vertical slice (walkthrough, 14 min) — Implement the smallest useful slice in learning/remote-data.ts.
- Represent loading, empty, error, and ready states.
- Connect discriminated union to a working example.
- Verify and harden (exercise, 9 min) — Render an empty state separately from loading.
- Avoid accidental any at API boundaries.
- Record one risk or follow-up before moving on.
- Structural Types, Unions, and State: Structural typing (concept, 35 min) — TypeScript compares object shapes. If two values have compatible members, they are assignable even if their type names differ. This is flexible for JavaScript code, but important domain identifiers often deserve branded or opaque wrappers.
- Retained source example: Shape compatibility
type Customer = { id: string; name: string };
type Account = { id: string; name: string };
const account: Account = { id: 'a_1', name: 'Ops' };
const customer: Customer = account; // Allowed: same shape.
- Structural Types, Unions, and State: Invalid states should be unrepresentable (walkthrough, 35 min) — Boolean flags often allow impossible combinations. A discriminated union keeps each state explicit and gives the compiler a field to narrow on.
- Retained source example: Request state union
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading'; startedAt: Date }
| { status: 'success'; data: T }
| { status: 'error'; message: string; retryable: boolean };
function label<T>(state: LoadState<T>): string {
switch (state.status) {
case 'idle': return 'Ready';
case 'loading': return `Started ${state.startedAt.toISOString()}`;
case 'success': return 'Loaded';
case 'error': return state.retryable ? 'Try again' : state.message;
}
}
- Structural Types, Unions, and State: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Represent workflows as unions instead of clusters of optional fields.
- Practice: Accept readonly inputs when a function should not mutate caller-owned data.
- Practice: Use literal types for protocol and domain states.
- Avoid: Adding optional properties until every object shape means too many things.
- Avoid: Forgetting that same-shaped domain objects are structurally compatible.
- Avoid: Using boolean flags where a state machine would be clearer.
- Structural Types, Unions, and State: references (review, 2 min) — Original references retained from the legacy library.
- TypeScript handbook: Object types: https://www.typescriptlang.org/docs/handbook/2/objects.html
- TypeScript handbook: Everyday types: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
- Narrowing Unknown Values: Control-flow analysis (concept, 30 min) — TypeScript tracks checks through branches, returns, and assignments. A good guard does real runtime work and gives the compiler a smaller type afterward.
- Retained source example: Guarding unknown input
type Env = { DATABASE_URL: string; NODE_ENV: 'development' | 'test' | 'production' };
function isEnv(value: unknown): value is Env {
return typeof value === 'object'
&& value !== null
&& 'DATABASE_URL' in value
&& 'NODE_ENV' in value;
}
function readEnv(value: unknown): Env {
if (!isEnv(value)) throw new Error('Invalid environment');
return value;
}
- Narrowing Unknown Values: Exhaustive checks (walkthrough, 30 min) — When every union member is handled, the remaining value becomes never. Passing that value to a helper makes future missing cases fail during type checking.
- Retained source example: assertNever pattern
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
type Job = { kind: 'email' } | { kind: 'webhook' };
function queueName(job: Job): string {
switch (job.kind) {
case 'email': return 'mail';
case 'webhook': return 'http';
default: return assertNever(job);
}
}
- Narrowing Unknown Values: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Keep guards small and test them like other boundary code.
- Practice: Use exhaustive switch checks for domain unions that may evolve.
- Practice: Prefer explicit failure messages at runtime boundaries.
- Avoid: Writing predicates that only cast and do not validate.
- Avoid: Forgetting that typeof null is object.
- Avoid: Checking property existence but not property value type.
- Narrowing Unknown Values: references (review, 2 min) — Original references retained from the legacy library.
- TypeScript handbook: Narrowing: https://www.typescriptlang.org/docs/handbook/2/narrowing.html
- Schemas, Parsers, and Boundaries: Parse, do not pretend (concept, 38 min) — External data starts as unknown. A parser checks the runtime structure and returns a trusted type only when validation succeeds. Libraries such as Zod, Valibot, and io-ts help keep schemas and inferred types together.
- Retained source example: Zod parser boundary
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().min(1),
email: z.string().email(),
createdAt: z.string().datetime(),
});
type UserDto = z.infer<typeof UserSchema>;
function parseUser(value: unknown): UserDto {
return UserSchema.parse(value);
}
- Schemas, Parsers, and Boundaries: Validation belongs at the edge (walkthrough, 38 min) — Validate once when data crosses into your system: HTTP request bodies, webhook events, environment variables, queue messages, and database documents from schemaless stores. After validation, pass domain types through the core.
- Retained source example: Route boundary shape
async function createUserRoute(request: Request): Promise<Response> {
const raw: unknown = await request.json();
const input = CreateUserSchema.parse(raw);
const user = await userService.create(input);
return Response.json(user, { status: 201 });
}
- Schemas, Parsers, and Boundaries: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Treat JSON, env vars, queues, webhooks, and user input as unknown.
- Practice: Return validation errors that are useful but do not leak secrets.
- Practice: Infer TypeScript types from schemas when a library supports it.
- Avoid: Replacing validation with as SomeType.
- Avoid: Validating too late after invalid data has entered domain logic.
- Avoid: Leaking internal parser diagnostics directly to external clients.
- Schemas, Parsers, and Boundaries: references (review, 2 min) — Original references retained from the legacy library.
- Zod documentation: https://zod.dev/
- OWASP input validation cheat sheet: https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
Code example
TypeScript in learning/remote-data.ts.
type RemoteData<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "ready"; data: T }
| { status: "error"; message: string };
function titleFor<T>(state: RemoteData<T>) {
return state.status === "ready" ? "Loaded" : state.status;
}
Walkthrough examples
- TypeScript Component Foundations in a learning analytics dashboard — A team is extending a resilient React product surface and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: learning/remote-data.ts
- File: tests/typescript-component-foundations.spec
- File: docs/typescript-react/typescript-component-foundations.md
- Start from the provided TypeScript snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Type props by product meaning" before adding extra behavior.
- Write down how the implementation changes when discriminated union fails or becomes slow.
- Shape compatibility — Additional example retained from the legacy lesson.
- Retained source code:
type Customer = { id: string; name: string };
type Account = { id: string; name: string };
const account: Account = { id: 'a_1', name: 'Ops' };
const customer: Customer = account; // Allowed: same shape.
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Request state union — Additional example retained from the legacy lesson.
- Retained source code:
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading'; startedAt: Date }
| { status: 'success'; data: T }
| { status: 'error'; message: string; retryable: boolean };
function label<T>(state: LoadState<T>): string {
switch (state.status) {
case 'idle': return 'Ready';
case 'loading': return `Started ${state.startedAt.toISOString()}`;
case 'success': return 'Loaded';
case 'error': return state.retryable ? 'Try again' : state.message;
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Readonly collection boundary — Accept readonly input when the function does not need mutation. Return a new mutable array only if the caller should own the copy.
- Retained source code:
function newestFirst(items: readonly Date[]): Date[] {
return [...items].sort((a, b) => b.getTime() - a.getTime());
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Guarding unknown input — Additional example retained from the legacy lesson.
- Retained source code:
type Env = { DATABASE_URL: string; NODE_ENV: 'development' | 'test' | 'production' };
function isEnv(value: unknown): value is Env {
return typeof value === 'object'
&& value !== null
&& 'DATABASE_URL' in value
&& 'NODE_ENV' in value;
}
function readEnv(value: unknown): Env {
if (!isEnv(value)) throw new Error('Invalid environment');
return value;
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- assertNever pattern — Additional example retained from the legacy lesson.
- Retained source code:
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
type Job = { kind: 'email' } | { kind: 'webhook' };
function queueName(job: Job): string {
switch (job.kind) {
case 'email': return 'mail';
case 'webhook': return 'http';
default: return assertNever(job);
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Assertion function — Additional example retained from the legacy lesson.
- Retained source code:
function assertString(value: unknown, name: string): asserts value is string {
if (typeof value !== 'string' || value.length === 0) {
throw new Error(`${name} must be a non-empty string`);
}
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Zod parser boundary — Additional example retained from the legacy lesson.
- Retained source code:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().min(1),
email: z.string().email(),
createdAt: z.string().datetime(),
});
type UserDto = z.infer<typeof UserSchema>;
function parseUser(value: unknown): UserDto {
return UserSchema.parse(value);
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Route boundary shape — Additional example retained from the legacy lesson.
- Retained source code:
async function createUserRoute(request: Request): Promise<Response> {
const raw: unknown = await request.json();
const input = CreateUserSchema.parse(raw);
const user = await userService.create(input);
return Response.json(user, { status: 201 });
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- safeParse for recoverable validation — Additional example retained from the legacy lesson.
- Retained source code:
const result = UserSchema.safeParse(raw);
if (!result.success) {
return Response.json({ error: 'Invalid user', issues: result.error.issues }, { status: 400 });
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Convert a loose props object into an interface.
- Model one async panel with a union.
- Render an empty state separately from loading.
- Design a payment state: Write the discriminants you would use for a payment state union. The guided runner checks for stable state names, not executable code.
- Hint: Think of lifecycle, not UI labels.
- Hint: Include at least pending, succeeded, and failed.
- Accepted answers: pending | succeeded | failed
- Guard checklist: Name two runtime checks needed before treating unknown JSON as an object with properties. The guided runner checks terminology only.
- Hint: Objects can be null.
- Hint: Property checks do not prove property value types.
- Accepted answers: typeof | null | in
- List trust boundaries: Name two inputs that should be treated as unknown until validated. The guided runner checks for common boundary terms.
- Accepted answers: request | webhook | environment | queue | database
- Apply: Identify trust boundaries that require runtime validation.
Checklist
- Type props by product meaning
- Parse external data
- Render every state
- Remove stray any usage
Quiz prompts
- Where should unknown API JSON usually become trusted typed data? — Parsing near the API boundary keeps the rest of the UI working with known shapes.
- A teammate wants to hide props contract inside a convenient helper. What should you check first? — Place props contract at the boundary that keeps learning analytics dashboard behavior explicit, testable, and reviewable.
- Which artifact best proves this TypeScript React lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Basic UI contracts: a teammate says the happy path works, but "API parsing boundary" is still implicit. What should you ask for before merging? — API parsing boundary belongs in the basic stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this basic TypeScript React slice. Which evidence is strongest? — Build the basic React feature contract: parsed input, discriminated remote state, presentational components, and a short note naming what cannot enter the render boundary.
- What makes a discriminated union easy to narrow? — The compiler can use a shared literal field to determine which member is active.
- What does an assertion function do? — The asserts return type tells TypeScript that successful return proves a condition.
- Where should runtime validation usually happen? — Boundary validation keeps the core code typed without scattering repeated checks.
Flashcards
- Basic UI contracts: what decision does "API parsing boundary" force you to make? Parse unknown server data once so components render trusted product types. Evidence prompt: Wrap one API response in a parser and pass only a typed view model into the component.
- Basic UI contracts: what decision does "Discriminated remote data" force you to make? Represent loading, ready, empty, and error as a union instead of scattered booleans. Evidence prompt: Replace three nullable fields or booleans with one discriminated state type.
- Basic UI contracts: what decision does "Presentational component contract" force you to make? Keep rendering components small, named, and testable by passing only the props they actually display. Evidence prompt: Extract one presentational component and document its required and optional props.
- In TypeScript React, what should you remember about props contract? props contract matters here because it supports "Type component props clearly.".
- In TypeScript React, what should you remember about discriminated union? discriminated union matters here because it supports "Represent loading, empty, error, and ready states.".
- In TypeScript React, what should you remember about unknown JSON? unknown JSON matters here because it supports "Avoid accidental any at API boundaries.".
- In TypeScript React, what should you remember about component composition? component composition matters here because it supports "Type component props clearly.".
Labs
- Ship a typescript component foundations slice — Extend a resilient React product surface with a small but reviewable feature that proves the lesson's architecture in code.
- Build the basic React feature contract: parsed input, discriminated remote state, presentational components, and a short note naming what cannot enter the render boundary.
- Wrap one API response in a parser and pass only a typed view model into the component.
- Replace three nullable fields or booleans with one discriminated state type.
- Extract one presentational component and document its required and optional props.
- Convert a loose props object into an interface.
- Model one async panel with a union.
- The lab demonstrates the basic ui contracts outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: API parsing boundary, Discriminated remote data, Presentational component contract.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready typescript component foundations (Core) — Build the basic React feature contract: parsed input, discriminated remote state, presentational components, and a short note naming what cannot enter the render boundary.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from learning/remote-data.ts plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL