Typescript

타입을 좁혀 정확한 타입을 얻어내자

비숑주인 2025. 4. 2. 19:32

TypeScript를 사용하다 보면 다양한 타입이 섞인 상황에서 정확한 타입을 식별해 사용하는 것이 중요하다. 이를 가능하게 하는 기법이 바로 타입 좁히기(Narrowing) 이다. 유니언 타입에서 특정 타입만을 골라내어 사용하는 방식으로, TypeScript의 제어 흐름 분석(Control Flow Analysis) 에 기반한다.

 

typeof를 이용한 타입 좁히기

가장 기초적인 타입 좁히기 방법은 typeof 연산자를 활용하는 것이다.

function strOrNum(param: string | number) {
  if (typeof param === 'string') {
    param; // string
  } else if (typeof param === 'number') {
    param; // number
  } else {
    param; // never
  }
}

 

마지막 else 블록에서 타입이 never가 되는 것에 주목해야 한다. 이는 TypeScript가 코드 흐름을 분석하여 string도 number도 아닌 값은 존재할 수 없음을 인식했기 때문이다.

 

null과 undefined 구분하기

단순히 typeof만으로는 null과 undefined를 명확히 구분할 수 없다. 예를 들어 아래의 코드는 의도한 대로 동작하지 않는다.

function strOrNullOrUndefined(param: string | null | undefined) {
  if (typeof param === 'undefined') {
    param; // undefined
  } else if (param) {
    param; // string
  } else {
    param; // string | null
  }
}

 

param이 빈 문자열 ''이면 falsy로 평가되므로, else 블록에서 여전히 string일 수 있다. 또한 typeof null === 'object'이므로 null도 typeof로는 판별할 수 없다.

 

올바른 구분법

function strOrNullOrUndefined(param: string | null | undefined) {
  if (param === undefined) {
    param; // undefined
  } else if (param === null) {
    param; // null
  } else {
    param; // string
  }
}

 

=== 연산자를 활용하면 null과 undefined를 명확히 구분할 수 있다.

 

boolean의 리터럴 구분

boolean 타입도 true, false로 좁힐 수 있다.

function trueOrFalse(param: boolean) {
  if (param) {
    param; // true
  } else {
    param; // false
  }
}

 

배열인지 확인하기 - Array.isArray

배열 여부는 Array.isArray로 구분할 수 있다.

 

function strOrNumArr(param: string | number[]) {
  if (Array.isArray(param)) {
    param; // number[]
  } else {
    param; // string
  }
}

 

또는 typeof param === 'string'으로도 같은 결과를 얻을 수 있다.

 

클래스 인스턴스 구분 - instanceof

클래스 인스턴스인지 확인할 때는 instanceof를 사용한다.

class A {}
class B {}

function classAorB(param: A | B) {
  if (param instanceof A) {
    param; // A
  } else {
    param; // B
  }
}

 

인터페이스(타입) 기반 객체 구분 - in 연산자

interface는 런타임에 존재하지 않기 때문에 instanceof를 사용할 수 없다. 대신 in 연산자를 사용해 속성의 존재 여부로 구분한다.

interface X {
  width: number;
  height: number;
}

interface Y {
  length: number;
  center: number;
}

function objXorY(param: X | Y) {
  if ('width' in param) {
    param; // X
  } else {
    param; // Y
  }
}

 

이 방법은 런타임에 실제 존재하는 속성 기준으로 판단하기 때문에 신뢰할 수 있다.

 

브랜드 속성(Discriminated Union) 사용

공통된 "_type"과 같은 식별자(discriminant)를 사용하여 객체를 구분하는 방식이다.

interface Money {
  _type: 'money';
  amount: number;
  unit: string;
}

interface Liter {
  _type: 'liter';
  amount: number;
  unit: string;
}

function moneyOrLiter(param: Money | Liter) {
  if (param._type === 'money') {
    param; // Money
  } else {
    param; // Liter
  }
}

 

타입 서술 함수(Type Predicate)

직접 타입을 구분하는 함수를 만들 경우, 반환 타입에 타입 서술(Type Predicate) 을 명시해야 한다.

 

function isMoney(param: Money | Liter): param is Money {
  return param._type === 'money';
}

function moneyOrLiter(param: Money | Liter) {
  if (isMoney(param)) {
    param; // Money
  } else {
    param; // Liter
  }
}

 

 

param is Money는 "이 함수가 true를 반환한다면 param은 Money 타입이다"를 의미한다. 단, param is Liter처럼 잘못 선언하면 타입이 반대로 좁혀지므로 주의해야 한다.

 

 

타입 좁히기를 통해 유니언 타입에서 정확한 타입을 얻어낼 수 있다. 아래는 타입 좁히기의 대표적인 방법들이다:


구분 방식 사용 방법 예시
기본 타입 typeof typeof param === 'string'
null/undefined === null, === undefined  
배열 Array.isArray()  
클래스 인스턴스 instanceof param instanceof A
객체 속성 기반 'prop' in param 'width' in param
브랜드 속성 _type 식별자 비교 param._type === 'money'
타입 서술 함수 param is 타입 리턴 타입 사용 function isMoney(...)