타입 좁히기 심화 — 제어 흐름 분석과 exhaustiveness 체크
제어 흐름 분석(Control Flow Analysis)은 TypeScript가 코드의 분기, 할당, 반환 을 추적해서 각 위치에서의 타입을 자동으로 좁혀 주는 메커니즘입니다.
제어 흐름 분석이 동작하는 경우
할당으로 좁히기
let value: string | number;
value = 'hello';
// value: string — 할당으로 좁혀짐
console.log(value.toUpperCase()); // OK
value = 42;
// value: number — 재할당으로 다시 좁혀짐
console.log(value.toFixed(2)); // OK
조건문 이후 좁히기
function process(value: string | number | null) {
if (value === null) {
return; // 여기서 반환하면...
}
// value: string | number — null이 제거됨
if (typeof value === 'string') {
// value: string
return value.toUpperCase();
}
// value: number — string도 제거됨
return value.toFixed(2);
}
논리 연산자로 좁히기
function greet(name: string | null) {
// && 연산자
const upper = name && name.toUpperCase(); // name이 truthy일 때만 실행
// || 연산자
const safe = name || '기본값'; // name이 falsy면 기본값
// ?? 연산자 (null/undefined만 체크)
const nullSafe = name ?? '기본값'; // null/undefined일 때만 기본값
}
Exhaustiveness Check 패턴
모든 경우를 빠짐없이 처리했는지 컴파일 타임에 보장 하는 패턴입니다.
never를 이용한 방법
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
default: {
const _exhaustive: never = shape;
return _exhaustive;
}
}
}
헬퍼 함수로 추출
function assertNever(value: never, message?: string): never {
throw new Error(message ?? `처리되지 않은 값: ${JSON.stringify(value)}`);
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
case 'triangle': return (shape.base * shape.height) / 2;
default: return assertNever(shape);
}
}
새 멤버 추가 시
// Rectangle을 추가하면
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number }
| { kind: 'rectangle'; width: number; height: number }; // 새로 추가
// getArea 함수에서 컴파일 에러 발생!
// Error: Argument of type '{ kind: "rectangle"; ... }' is not assignable to type 'never'
// → rectangle case를 추가하라는 알림
타입 좁히기가 동작하지 않는 경우
콜백 함수 안에서
function example(value: string | null) {
if (value !== null) {
// value: string — 여기서는 좁혀짐
setTimeout(() => {
// ⚠️ value: string | null — 콜백 안에서는 좁히기가 유지되지 않음!
// TypeScript는 콜백 실행 시점에 value가 변했을 수 있다고 판단
}, 1000);
}
}
// 해결: 지역 변수에 할당
function exampleFixed(value: string | null) {
if (value !== null) {
const safeValue = value; // string으로 고정
setTimeout(() => {
console.log(safeValue.toUpperCase()); // OK
}, 1000);
}
}
속성 접근에서
interface Config {
value?: string;
}
function process(config: Config) {
if (config.value) {
// config.value: string — 좁혀짐
doSomething(); // 이 함수가 config.value를 바꿀 수 있음
// config.value: string | undefined — 다시 넓어질 수 있음
// TypeScript 5.x에서는 이런 경우를 더 잘 처리
}
}
해결: 구조 분해 할당
function process(config: Config) {
const { value } = config; // 지역 변수로 추출
if (value) {
// value: string — 안정적으로 좁혀짐
doSomething();
console.log(value.toUpperCase()); // OK
}
}
satisfies와 exhaustiveness
// satisfies로도 exhaustiveness를 체크할 수 있음
type EventHandler = Record<'click' | 'scroll' | 'keypress', () => void>;
const handlers = {
click: () => console.log('클릭'),
scroll: () => console.log('스크롤'),
keypress: () => console.log('키입력'),
// 하나라도 빠지면 에러
} satisfies EventHandler;
정리
- TypeScript의 제어 흐름 분석은 if, switch, return 등을 추적해서 타입을 좁힌다
never와assertNever함수로 exhaustiveness check를 구현한다- 콜백 함수 안에서는 타입 좁히기가 유지되지 않을 수 있다
- 지역 변수나 구조 분해 할당으로 안정적인 좁히기를 확보한다
- 새로운 유니온 멤버 추가 시 처리하지 않은 곳을 자동으로 찾을 수 있다
댓글 로딩 중...