Menu

TypeScript 타입 좁히기(narrowing)와 제어 흐름 분석

좁히기는 TypeScript가 if, switch 안에서, 또는 early return 뒤에 string | number 같은 넓은 타입을 구체적인 타입으로 바꾸는 방식입니다. typeof, 참 같은 값 검사, 동등 비교, in, instanceof, 대입, 타입 서술어, 판별 유니언까지 모든 좁히기 형태를 한곳에 정리합니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

타입 좁히기는 코드가 이미 수행한 검사를 바탕으로, TypeScript가 코드의 특정 지점에서 값에 대해 더 구체적인 타입을 알아내는 것입니다. string | number 매개변수는 if (typeof x === "string") 안에서 string이 되고 else에서 number가 됩니다.

검사 전에 value.toFixed(2)를 호출하면 string에는 toFixed가 없으므로 컴파일 오류입니다. 검사는 평범한 JavaScript로 런타임에 실행되고, 좁히기는 컴파일러가 그것을 읽고 타입을 조정하는 것입니다. 추가로 출력되는 것은 없습니다.

제어 흐름 분석

TypeScript는 함수의 모든 경로를 따라갑니다. if/else, early return과 throw, switch, 반복문, 단락 연산자 &&, ||, ??, ?:입니다. 각 지점에서 변수의 타입은 그곳에서 아직 가능한 것입니다.

early return 스타일("가드 절")이 가장 읽기 쉬운 좁히기 방법입니다. 특이한 경우를 먼저 처리하면 함수의 나머지는 깔끔한 타입으로 작업합니다.

좁히는 모든 방법

형태예좁히는 대상
typeoftypeof x === "string"원시 타입과 함수
참 같은 값if (x)null, undefined, falsy 리터럴 제거
동등 비교x === "a", x == null, x !== undefined리터럴, null, undefined
in"swim" in pet속성 기준의 객체 유니언
instanceoferr instanceof TypeError클래스 인스턴스
Array.isArrayArray.isArray(x)배열과 그 밖의 모든 것
대입x = 5대입된 타입으로
타입 서술어function isUser(x: unknown): x is User검사할 수 있는 무엇이든
단언 함수function assertUser(x: unknown): asserts x is User호출 뒤의 모든 곳
판별자 속성switch (shape.kind)태그 유니언

마지막 세 가지는 타입 가드와 판별 유니언 페이지에서 다룹니다. 나머지는 아래에 있습니다.

참 같은 값으로 좁히기

if (x)는 null과 undefined(그리고 false, 0, "" 리터럴 타입)를 없앱니다. 짧지만 고전적인 함정이 하나 있습니다. 0과 ""는 falsy이므로 올바른 값이 없는 값으로 취급됩니다.

숫자와 문자열은 undefined나 null과 명시적으로 비교하세요(또는 ??를 쓰세요). 객체, 배열, 함수는 절대 falsy가 아니므로 참 같은 값 검사로 충분합니다.

동등 비교로 좁히기

===, !==, ==, !=는 양쪽을 모두 좁힙니다. 리터럴과 비교하면 그 리터럴로 좁혀지고, == null(느슨한 동등 비교)은 검사 한 번으로 null과 undefined를 모두 잡으며, 느슨한 동등 비교가 관용적으로 쓰이는 유일한 곳입니다.

두 변수를 비교하면 둘이 공통으로 가질 수 있는 것으로 둘 다 좁혀집니다. a: string | number와 b: string | boolean이 a === b를 통과하면 if 안에서 둘 다 string입니다.

in 연산자

"key" in obj는 객체 타입의 유니언을 그 속성을 가진(또는 가질 수 있는) 멤버로 좁힙니다.

in은 객체라는 것을 알고 나면 unknown에도 동작합니다. typeof v === "object" && v !== null && "id" in v 뒤에 TypeScript는 v에 unknown 타입의 id 속성이 있다는 것을 압니다. 직접 설계하는 유니언이라면 메서드가 있는지 찔러 보기보다 공유 태그 속성(kind: "fish")이 더 명확합니다. 이 패턴을 판별 유니언이라고 합니다.

대입으로 좁히기

변수에는 선언된 타입과, 대입을 따라가는 좁혀진 타입이 있습니다. 값을 대입하면 선언된 타입 범위 안에서 그 값의 타입으로 좁혀집니다.

좁히기가 풀리는 곳

좁히기는 지역적이고 보수적입니다. 몇 가지 상황에서 초기화됩니다.

  • 다른 표현식. obj.name을 검사하면 obj.name(과 obj["name"])이 좁혀지지만, key가 리터럴이 아닌 string 변수일 때의 obj[key]와 검사 전에 만든 복사본은 좁혀지지 않습니다.
  • 콜백과 재대입. 콜백 안에서 좁혀진 let은 콜백이 만들어진 뒤에 다시 대입되지 않을 때만 좁혀진 채로 남습니다. const나 다시 대입되지 않는 매개변수는 좁혀진 채로 남습니다.
  • 헬퍼에 숨은 검사. isString(x: unknown): boolean 함수는 컴파일러에 아무것도 알려 주지 않습니다. 반환 타입을 타입 서술어 x is string으로 주면 그 호출이 typeof처럼 좁힙니다.

컴파일러는 index.ts(5,38): error TS18048: 'x' is possibly 'undefined'.를 보고합니다. 콜백은 나중에, x = undefined 뒤에 실행될 수도 있습니다. 마지막 대입을 지우면(또는 if 안에서 값을 const에 복사하면) 컴파일되고 5를 두 번 출력합니다.

boolean을 반환하는 헬퍼는 무엇을 증명하는지 선언해서 고칠 수 있습니다. 그것이 타입 가드입니다.

function isString(value: unknown): value is string {
  return typeof value === "string";
}

TypeScript 5.5부터 컴파일러는 단순한 화살표 함수에 대해 이런 서술어를 추론합니다. 그래서 이제 list.filter((x) => x !== undefined)가 undefined 없는 배열을 반환합니다.

자주 묻는 질문

TypeScript에서 타입 좁히기란 무엇인가요?

코드가 수행한 검사를 바탕으로 TypeScript가 블록 안에서 변수의 타입을 더 구체적으로 만드는 것입니다. if (typeof x === "string") 뒤에 string | number는 if 안에서는 string, else에서는 number입니다. 컴파일러는 if, else, return, switch, &&, ||, ?:를 따라가며 각 지점의 타입을 알아내는데, 이를 제어 흐름 분석이라고 합니다.

TypeScript가 타입을 좁히지 않는 이유는 무엇인가요?

흔한 원인: 검사한 표현식과 쓰는 표현식이 다른 경우(obj.a를 검사하고 string 타입의 key로 obj[key]를 씀), 콜백이 만들어진 뒤에 다시 대입되는 let이라서 콜백이 좁히기를 잃는 경우, 검사가 타입 서술어 x is T 대신 평범한 boolean을 반환하는 헬퍼 안에 숨어 있는 경우입니다.

타입 좁히기는 런타임에 동작하나요?

검사는 동작합니다. typeof, instanceof, in, ===는 실행되는 평범한 JavaScript입니다. 좁히기 자체는 컴파일 시점에만 일어납니다. TypeScript가 런타임 검사를 읽고 정적 타입을 그에 맞게 조정할 뿐, 출력된 JavaScript에는 아무것도 더해지지 않습니다.

TypeScript에서 unknown 타입은 어떻게 좁히나요?

같은 검사를 씁니다: typeof value === "string", Array.isArray(value), value instanceof Date, 객체라면 typeof value === "object" && value !== null && "id" in value. 재사용할 검사라면 반환 타입이 value is T인 타입 가드 함수를 작성하세요.

TypeScript에서 배열의 undefined를 걸러 내려면 어떻게 하나요?

TypeScript 5.5부터는 콜백을 타입 서술어로 추론하므로 items.filter((x) => x !== undefined)가 undefined 없는 T[]를 반환합니다. 이전 버전에서는 서술어를 직접 쓰세요: items.filter((x): x is T => x !== undefined).

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기