iBetter Books
수정

Ch 04. never 타입과 완전성 검사

never는 절대 발생할 수 없는 타입입니다. 모든 타입이 좁혀져서 더 이상 가능한 타입이 없을 때 나타납니다. 이 특성을 활용하면 switch 문의 case가 누락되지 않았는지 컴파일 타임에 강제할 수 있습니다.

never 타입이란

never는 값이 존재할 수 없는 타입입니다. 두 가지 상황에서 등장합니다.

첫 번째는 함수가 절대 반환하지 않을 때입니다.

// 파일: src/never/never-basic.ts// 항상 throw — 반환값이 없으므로 neverfunction fail(message: string): never {  throw new Error(message);}// 무한 루프 — 역시 neverfunction infiniteLoop(): never {  while (true) {    // ...  }}

두 번째는 모든 유니온 멤버가 좁혀진 후 남는 것이 없을 때입니다.

// 파일: src/never/never-narrowing.tstype Color = "red" | "green" | "blue";function describeColor(color: Color): string {  if (color === "red") return "빨강";  if (color === "green") return "초록";  if (color === "blue") return "파랑";  // 여기에 도달하면 color의 타입은 never  // 실제로는 도달할 수 없음  const _exhaustive: never = color;  return _exhaustive;}

exhaustive check 패턴

새 유니온 멤버가 추가될 때 처리 코드를 빠뜨리면 버그가 발생합니다. never 변수 할당을 이용해 컴파일러가 이 누락을 잡도록 만들 수 있습니다.

// 파일: src/never/exhaustive.ts// 유틸리티 함수 — 절대 호출되면 안 되는 코드 경로를 표시function assertNever(value: never): never {  throw new Error(`처리되지 않은 케이스: ${JSON.stringify(value)}`);}type Shape =  | { kind: "circle"; radius: number }  | { kind: "rectangle"; width: number; height: number }  | { kind: "triangle"; base: number; height: number };function getArea(shape: Shape): number {  switch (shape.kind) {    case "circle":      return Math.PI * shape.radius ** 2;    case "rectangle":      return shape.width * shape.height;    case "triangle":      return (shape.base * shape.height) / 2;    default:      // shape.kind가 위 세 가지 중 하나가 아닌 경우는 없으므로      // default에서 shape의 타입은 never      return assertNever(shape);  }}

여기에 새 shape을 추가하면 어떻게 될까요.

// 파일: src/never/exhaustive-broken.tstype Shape =  | { kind: "circle"; radius: number }  | { kind: "rectangle"; width: number; height: number }  | { kind: "triangle"; base: number; height: number }  | { kind: "pentagon"; sides: number; sideLength: number }; // 새로 추가// 위의 getArea 함수는 이제 컴파일 오류 발생:// Argument of type '{ kind: "pentagon"; sides: number; sideLength: number }'// is not assignable to parameter of type 'never'.

pentagon case를 처리하지 않으면 default에서 shape의 타입이 never가 아니라 pentagon 타입이 되고, assertNever에 전달할 수 없게 됩니다. 컴파일 오류가 누락을 알려주는 것입니다.

switch에서 완전성 검사 적용하기

실전 코드에서 자주 쓰는 패턴입니다.

// 파일: src/never/exhaustive-redux.tstype Action =  | { type: "INCREMENT"; by: number }  | { type: "DECREMENT"; by: number }  | { type: "SET"; value: number }  | { type: "RESET" };interface State {  count: number;  history: number[];}function assertNever(value: never): never {  throw new Error(`처리되지 않은 액션: ${(value as any).type}`);}function reducer(state: State, action: Action): State {  switch (action.type) {    case "INCREMENT":      return {        count: state.count + action.by,        history: [...state.history, state.count],      };    case "DECREMENT":      return {        count: state.count - action.by,        history: [...state.history, state.count],      };    case "SET":      return {        count: action.value,        history: [...state.history, state.count],      };    case "RESET":      return { count: 0, history: [] };    default:      return assertNever(action);  }}

never로 불가능한 타입 조합 표현하기

never는 조건부 타입에서도 자주 등장합니다. 특정 타입 조합을 금지할 때 사용합니다.

// 파일: src/never/never-conditional.ts// string이면 never, 아니면 T를 반환 — string을 걸러내는 타입type NonString<T> = T extends string ? never : T;type A = NonString<string | number | boolean>;// A = number | boolean  (string은 never로 대체되어 사라짐)// 두 타입이 겹치지 않게 강제하는 유틸리티type Exclusive<T, U> = T & { [K in keyof U]?: never };interface HasName {  name: string;}interface HasEmail {  email: string;}// name만 있거나 email만 있어야 함 — 둘 다 있으면 오류type NameOnly = Exclusive<HasName, HasEmail>;type EmailOnly = Exclusive<HasEmail, HasName>;

타입 단언 없이 never 활용하기

// 파일: src/never/never-practical.ts// 빈 배열은 never[]로 추론됩니다const empty = []; // never[]// 타입을 지정하면 해결const numbers: number[] = [];// 함수 반환으로 never 활용function throwError(code: number, message: string): never {  const error = new Error(message);  (error as any).code = code;  throw error;}function getUserOrThrow(users: Map<string, string>, id: string): string {  const user = users.get(id);  if (!user) {    throwError(404, `사용자 ${id}를 찾을 수 없습니다`);    // throwError가 never를 반환하므로 이 줄은 도달 불가    // TypeScript는 이 사실을 알고 user를 string으로 처리합니다  }  return user; // user는 string — undefined 아님}

never 타입은 TypeScript 타입 시스템의 바닥(bottom type)입니다. 다른 모든 타입에 할당할 수 있지만, 어떤 타입도 never에 할당할 수 없습니다. 이 특성을 이용해 완전성 검사를 작성하면 코드베이스가 성장할 때 처리 누락을 컴파일러가 자동으로 알려줍니다.