C.W.K.
Lesson 04 of 06 · published

TypeScript 6 Strict — Typing React + Async + SSE

~16 min · typescript, strict, types

Level 0Curious
0 XP0/52 lessons0/16 achievements
0/100 XP to next level100 XP to go0% complete

Strict mode isn't optional

Every tsconfig.json in cwkPippa has "strict": true. Not negotiable. The reason isn't dogma — it's that the bugs strict catches at compile time are the exact bugs that show up at 2am when you can't reproduce them.

strictNullChecks catches the 'cannot read property of undefined' family. noImplicitAny stops a single sloppy parameter from leaking any through your whole call graph. strictFunctionTypes makes function parameter variance honest.

Discriminated unions are how I model server messages

The SSE stream from my backend sends typed events: text deltas, tool uses, tool results, errors, the final done. Modeling that as a single discriminated union is what makes the frontend code actually safe — TypeScript narrows the type inside each switch arm, and you can't forget a case (set noFallthroughCasesInSwitch: true and you can't write a buggy switch).

Generic hooks — typed without losing inference

Your custom hooks should accept generics so callers don't lose type information. useFetch<T>() instead of useFetch() returning any.

The principle: A type that ends in any is a type that doesn't exist. If you're tempted to use as any, stop and figure out what you actually want the contract to be. Sometimes the answer is unknown + a runtime guard. That's still better than any.

Code

Discriminated union for SSE events·ts
type SSEEvent =
  | { type: 'delta'; text: string }
  | { type: 'thinking'; text: string }
  | { type: 'tool_use'; name: string; input: unknown }
  | { type: 'tool_result'; tool_use_id: string; output: string }
  | { type: 'usage'; input_tokens: number; output_tokens: number }
  | { type: 'emotion'; emotion: string }
  | { type: 'done' }
  | { type: 'error'; message: string };

function handleEvent(e: SSEEvent) {
  switch (e.type) {
    case 'delta': return appendText(e.text);
    case 'thinking': return appendThinking(e.text);
    case 'tool_use': return startTool(e.name, e.input);
    case 'tool_result': return finishTool(e.tool_use_id, e.output);
    case 'usage': return recordCost(e.input_tokens, e.output_tokens);
    case 'emotion': return setAvatarEmotion(e.emotion);
    case 'done': return finalize();
    case 'error': return showError(e.message);
    // exhaustive — TS errors if a case is missing
  }
}

Progress

Progress is local-only — sign in to sync across devices.