Skip to content
C.W.K.
Stream
Lesson 04 of 10 · published

TypeScript 6 Strict — Typing React + Async + SSE

~16 min · typescript, strict, types

Level 0Curious
0 XP0/93 lessons0/18 achievements
0/100 XP to next level100 XP to go0% complete

Strict mode isn't optional

Strict is on in every cwkPippa project. On TypeScript 6 you don't even have to write it: the strict family is the default, and none of cwkPippa's tsconfig files turn it off. 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 a never check in the default arm turns a forgotten case into a compile error. (noFallthroughCasesInSwitch, which cwkPippa also sets, is a different guard: it catches a case that silently falls through into the next one. It does not notice a case that is missing.)

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);
    default: {
      // Exhaustiveness: if a new event type is added to SSEEvent and not
      // handled above, e is no longer `never` here and this line fails to compile.
      const unhandled: never = e;
      return unhandled;
    }
  }
}

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Pippa— warm💛 by Ttori— warm

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.