본문 바로가기
C.W.K.
Stream
Lesson 04 of 08 · published

TypeScript 6 Strict — React + Async + SSE 타이핑

~16 min · typescript, strict, types

Level 0호기심
0 XP0/69 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

Strict 모드는 옵션 아냐

cwkPippa 의 모든 tsconfig.json"strict": true. 협상 안 해. dogma 때문이 아니야. strict가 컴파일할 때 잡는 오류는 대개 새벽 2시에만 나타나서 사람 약 올리는 종류거든.

strictNullChecks 는 'cannot read property of undefined' 패밀리 잡아. noImplicitAny 는 sloppy 한 parameter 하나가 call graph 전체에 any 새는 거 막아. strictFunctionTypes 는 함수 파라미터 variance 를 정직하게 만들어.

Discriminated union 으로 server 메시지 모델링

backend의 SSE stream은 text delta, tool use, tool result, error, 마지막 done 같은 typed event를 보내. 이걸 하나의 discriminated union으로 모델링하면 TypeScript가 각 switch arm에서 type을 좁혀 줘. 새 event를 추가하고 case를 빠뜨린 자리도 exhaustive check로 드러나.

Generic hook — inference 안 잃고 typed

custom hook은 generic을 받아서 caller의 type 정보를 보존해. useFetch<T>()처럼 반환 계약이 드러나는 편이, 아무 값이나 내놓는 useFetch()보다 훨씬 정직해.

원칙: any는 type 계약을 지워 버려. as any에 손이 갈 때는 멈추고 진짜로 원하는 contract가 뭔지 생각해. 경계에서 아직 모르는 값이라면 unknown으로 받고 runtime guard로 좁혀. 귀찮아도 거짓 확신보단 낫지.

Strict typing의 목적은 타입 체조가 아니야. server가 보내는 값, UI가 믿는 값, 사용자가 실제로 보는 값 사이에 검문소를 세우는 거야. 특히 SSE처럼 여러 event가 한 통로로 흐를 땐 "대충 이런 모양"이라는 믿음이 가장 위험해. 네트워크 경계에서 검증하고, union 안으로 들어온 뒤에는 compiler가 끝까지 따라오게 해. 그러면 오류 처리는 예외적인 땜질이 아니라 모델의 한 가지 정상 분기가 돼.

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.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.