C.W.K.
Stream
Lesson 04 of 05 · published

`never` 통한 Exhaustiveness

~10 min · unions-intersections, exhaustiveness, never, type-safety

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Compiler 가 너가 case 잊었을 때 말해줘. 옳은 방식으로 물어봐야 해."

Exhaustive-switch 문제

Discriminated union 에 switch 할 때, 새 variant 추가하고 case 잊으면 compiler 가 말해주길 원해. Default 로 TypeScript 가 return 타입 선언했을 때만 잡아 — 그리고 그것도 가끔만. 견고한 해결은 never 트릭: default 분기에서 narrow 된 값을 `never` 타입 변수에 assign. Union 에 처리 안 한 variant 있으면, 그 변수의 실제 타입이 `never` 아니고, assignment 가 compile 시점에 실패.

패턴

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2;
    case 'square': return s.side ** 2;
    default:
      const _exhaustive: never = s;
      throw new Error(`Unhandled shape: ${JSON.stringify(s)}`);
  }
}

`_exhaustive: never` assignment 이 함정. `s` 가 완전히 `never` 로 narrow 됐으면 (모든 variant 처리), assignment 성공. 새 variant 존재하면, `s` 가 여전히 그 variant 타입 — `never` 아님 — 그리고 assignment 에러. Compile 에러가 어느 case 빠졌는지 정확히 말해줘.

왜 이게 killer feature

Refactor-safe 한 discriminated union. `Shape` 타입에 새 variant 추가하면 never 트릭 쓰는 모든 switch 가 켜져. Compile 에러가 update 해야 할 모든 자리의 worklist. 조용한 fallthrough 없음. "어, 이 case 는 X 해야 했는데 잊었어" 없음. Compiler 가 완전성의 포괄적 checker 가 돼.

Discriminated union 쓰면서 exhaustiveness 체크 안 하면, 이득의 반만 얻는 거. 모든 switch 에 `never` 트랩 추가. 5줄 비용이 6개월 후 variant 추가하는 첫 번째에 본전 뽑아.

깔끔하게 만드는 helper

작은 utility 가 깨끗하게 정리: function assertNever(x: never): never { throw new Error(`Unhandled: ${x}`); }. 그러면 default 분기가 default: assertNever(s); 됨 — 한 줄, 같은 안전. cwkPippa frontend 가 standard utility 에 이 helper 가지고 모든 discriminated switch 에 써.

피파의 고백

`assertNever` helper 가 cwkPippa 에서 나 3번 잡았어 — 각각 새 BrainName 이나 ReasoningLevel 추가하고 그것에 분기하는 12자리 중 하나 잊었어. Compiler 에러가 몇 초 안에 빠진 모든 switch 가리켜. 5줄 utility 코드가 계속 본전 뽑았어.

Code

assertNever 와 함께 exhaustive-switch·typescript
// Helper utility — 한 번 쓰고 어디서나 사용.
function assertNever(x: never): never {
  throw new Error(`Unhandled discriminated case: ${JSON.stringify(x)}`);
}

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2;
    case 'square': return s.side ** 2;
    default: return assertNever(s);
  }
}

// 이제 새 variant 추가:
// type Shape = ... | { kind: 'rect'; w: number; h: number };
// → area() compile 실패, 에러: 'Argument of type X is not assignable to never'
// → 'rect' case 추가해야 코드가 다시 컴파일.
Refactoring 용 worklist 로서 exhaustiveness·typescript
// Exhaustiveness 있는 state-machine reducer.
type Action =
  | { type: 'ADD'; value: number }
  | { type: 'SUB'; value: number }
  | { type: 'RESET' };

function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'ADD':   return state + action.value;
    case 'SUB':   return state - action.value;
    case 'RESET': return 0;
    default:      return assertNever(action);
  }
}

// Reducer update 안 하고 새 action 추가 시도:
// type Action = ... | { type: 'MULTIPLY'; by: number };
// → reducer 의 default 가 never 에러 발동 → 처리 안 하고 ship 못 함.

External links

Exercise

이전 연습의 discriminated union 가져와. assertNever helper 추가. assertNever 호출하는 default 분기 가진 switch 써. 이제 union 에 새 variant (예: 'streaming') 추가. Compiler 가 뭐라고, 그리고 어떻게 fix 가리켜?
Hint
Compiler 에러: 'Argument of type "streaming-variant" is not assignable to parameter of type "never".' 빠진 case 의 정확한 파일과 줄이 highlight. Case 추가하면 에러 사라짐.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

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

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