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

never로 완전성 검사하기: 빠진 분기를 컴파일 오류로 바꿔

~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
"새 상태를 추가했을 때 고쳐야 할 모든 switch를 컴파일러가 작업 목록으로 만들어 주게 해."

기본 switch는 빠진 경우를 놓칠 수 있어

판별 가능한 유니언을 switch로 처리하다 새 변형을 추가하면 기존 함수들이 그 경우를 모를 수 있어. 반환 타입과 설정에 따라 일부 오류는 잡히지만, 모든 switch에서 확실히 알려 주는 패턴이 필요해.

모든 case 뒤의 default에서 남은 값을 never 매개변수에 넘기면 돼. 앞에서 모든 변형을 처리했다면 값은 never로 좁혀져 호출이 통과해. 하나라도 빠졌다면 아직 구체 변형 타입이 남아 never에 넣을 수 없다는 오류가 난다.

assertNever 도우미

function assertNever(x: never): never를 공용 도구로 만들면 각 switch의 default는 한 줄이면 돼. 실행 중 예상 밖의 값이 들어온 경우에는 예외를 던져 방어하고, 컴파일 시점에는 완전성 검사를 제공해.

새 변형을 유니언에 추가하는 순간 assertNever가 있는 모든 switch에 오류가 생겨. 사람이 검색으로 처리 지점을 찾는 대신 타입 그래프가 정확한 목록을 만들어 준다. 변경에 강한 모델의 진짜 이득이 여기서 드러나.

반환값이 없어도 작동해

부수 효과만 수행하는 switch에도 같은 패턴을 쓸 수 있어. 완전성 검사는 반환 타입이 아니라 남은 값이 never인지 확인하는 데서 나오기 때문이야. reducer, 이벤트 처리기, 로깅 분기 어디든 적용할 수 있다.

판별 가능한 유니언을 쓴다면 처리 지점에는 완전성 검사까지 붙여. 그래야 새 변형이 안전한 리팩터링 목록으로 바뀐다.

피파의 고백

새 상태 하나를 더하고 열두 switch 가운데 열한 곳만 고친 적이 있어. assertNever가 있던 자리만 즉시 알려 줬고, 없던 한 곳은 테스트까지 기다렸지. 그 뒤로 닫힌 유니언의 switch에는 마지막 증명 줄을 아끼지 않아.

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

이전 판별 합집합의 기본 분기에서 assertNever를 호출해. 새 streaming 상태를 추가했을 때 빠진 처리가 어느 줄의 오류로 드러나는지 확인해.
Hint
빠진 상태를 never 자리에 넘긴 곳에서 컴파일 오류가 나야 해. 오류 위치가 새 상태를 처리하지 않은 분기를 가리키는지 확인해.

Progress

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

댓글 0

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

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