제어 흐름 분석(Control Flow Analysis)은 TypeScript가 코드의 분기, 할당, 반환 을 추적해서 각 위치에서의 타입을 자동으로 좁혀 주는 메커니즘입니다.

제어 흐름 분석이 동작하는 경우

할당으로 좁히기

TYPESCRIPT
let value: string | number;

value = 'hello';
// value: string — 할당으로 좁혀짐
console.log(value.toUpperCase()); // OK

value = 42;
// value: number — 재할당으로 다시 좁혀짐
console.log(value.toFixed(2)); // OK

조건문 이후 좁히기

TYPESCRIPT
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);
}

논리 연산자로 좁히기

TYPESCRIPT
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를 이용한 방법

TYPESCRIPT
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;
    }
  }
}

헬퍼 함수로 추출

TYPESCRIPT
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);
  }
}

새 멤버 추가 시

TYPESCRIPT
// 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를 추가하라는 알림

타입 좁히기가 동작하지 않는 경우

콜백 함수 안에서

TYPESCRIPT
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);
  }
}

속성 접근에서

TYPESCRIPT
interface Config {
  value?: string;
}

function process(config: Config) {
  if (config.value) {
    // config.value: string — 좁혀짐

    doSomething(); // 이 함수가 config.value를 바꿀 수 있음

    // config.value: string | undefined — 다시 넓어질 수 있음
    // TypeScript 5.x에서는 이런 경우를 더 잘 처리
  }
}

해결: 구조 분해 할당

TYPESCRIPT
function process(config: Config) {
  const { value } = config; // 지역 변수로 추출

  if (value) {
    // value: string — 안정적으로 좁혀짐
    doSomething();
    console.log(value.toUpperCase()); // OK
  }
}

satisfies와 exhaustiveness

TYPESCRIPT
// 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 등을 추적해서 타입을 좁힌다
  • neverassertNever 함수로 exhaustiveness check를 구현한다
  • 콜백 함수 안에서는 타입 좁히기가 유지되지 않을 수 있다
  • 지역 변수나 구조 분해 할당으로 안정적인 좁히기를 확보한다
  • 새로운 유니온 멤버 추가 시 처리하지 않은 곳을 자동으로 찾을 수 있다
댓글 로딩 중...