"좁히기 연산자가 낱말이라면, 제어 흐름 분석은 그 낱말을 문장으로 묶는 문법이야."
컴파일러가 경로를 따라간다
TypeScript는 if와 switch의 각 분기, 대입, return, throw를 따라가며 프로그램의 어느 지점에 어떤 값이 도달할 수 있는지 계산해. 변수의 선언 타입은 그대로지만 각 줄에서 사용할 수 있는 더 좁은 타입은 흐름에 따라 달라진다.
if (x === null) return 다음 줄에는 null인 경로가 도달할 수 없어. 그래서 함수 나머지에서 x는 자동으로 null이 제거된 타입이 돼. 사람이 코드를 읽으며 하는 추론을 컴파일러가 보수적으로 따라 하는 거야.
조기 반환이 중첩을 줄여
잘못된 경우와 빈 경우를 함수 위에서 먼저 반환하면 아래의 주요 흐름은 좁은 타입으로 평평하게 이어져. else 안에 핵심 로직을 깊게 넣는 것보다 읽기 쉽고, 타입 검사도 함수 끝까지 같은 증거를 유지한다.
대입도 가능한 타입을 바꾼다
let name: string | null이 null일 때 기본 문자열을 대입하면 그 뒤의 name은 string이야. 컴파일러는 단순 조건뿐 아니라 값이 실제로 다시 정해졌다는 사실도 반영해. 다만 변수의 선언 타입 자체가 바뀌는 것은 아니어서 이후 null을 다시 대입할 수는 있어.
객체 속성과 함수 호출은 더 보수적이야
지역 변수는 쓰기 지점을 모두 볼 수 있지만 객체 속성은 다른 함수가 바꿀 수 있어. box.value를 좁힌 뒤 box를 외부 함수에 넘기면, 호출이 속성을 변경했을 가능성 때문에 좁히기가 유지되지 않을 수 있다.
필요하다면 좁힌 속성을 지역 상수에 복사하거나, 변경하지 않는다는 계약을 타입과 설계로 분명히 해. 컴파일러가 괜히 의심하는 것이 아니라 별칭을 통한 변경 가능성을 인정하는 거야.
대입을 통한 좁히기는 재할당할 수 있는 let에서 작동하고 const에는 해당하지 않아. 또 선언 자체를 바꾸는 것이 아니라 현재 함수의 특정 지점에서만 가능한 타입을 줄이는 분석이야.