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에 할당할 수 없습니다. 이 특성을 이용해 완전성 검사를 작성하면 코드베이스가 성장할 때 처리 누락을 컴파일러가 자동으로 알려줍니다.