Skip to content

타입 좁히기 (Type Narrowing) ​

타입 좁히기란, 하나의 값이 여러 타입 중 하나일 때 코드의 흐름(조건문, 검사 등)을 통해 보다 구체적인 타입으로 한정하는 과정이다.

주로 typeof, instanceof, 동등 비교, in 연산자, 사용자 정의 타입 가드 등을 통해 수행된다. 유니언 타입 |을 다룰 때 특히 자주 사용된다.


1. typeof 연산자 ​

typeof는 기본형(primitive) 타입 구분에 사용된다.
JS의 런타임 타입 검사와 동일하게 동작하며, 조건문 내부에서 TypeScript가 자동으로 타입을 좁혀준다.

ts
type Id = number | string;

let id: Id = 1;

if (typeof id === "string") {
  // 문자열 ID 경우 로직 처리
}

function getId(id: Id) {
  if (typeof id === "number") {
    return id;
  }

  return Number(id);
}

getId(1);
getId("1");
  • typeof는 실행 시점에 실제 값의 타입을 판별한다. null은 예외적으로 object를 반환하므로 주의

2. 동일성/동등성 좁히기 (Equality Narrowing) ​

값의 동등 비교 ===, !== 를 통해 타입을 구분할 수 있다.
리터럴 유니언("on" | "off") 타입에서 자주 사용된다.

ts
type Option = "on" | "off";

function power(option: Option) {
  if (option === "off") {
    console.log("power off");
  } else {
    console.log("power on");
  }
}

power("on");
power("off");
  • 비교문 안에서 option의 타입이 off 또는 on으로 좁혀진다.

3. in 연산자 ​

in 연산자는 객체에 특정 속성이 존재하는지를 확인해 타입을 좁힌다.
서로 다른 객체 구조를 가진 유니언 타입에서 자주 쓰인다.

ts
type iOS = { iMessage: () => void };
type android = { message: () => void };

function sendMessage(os: iOS | android) {
  if ("iMessage" in os) {
    os.iMessage(); // iOS로 좁혀진다
  } else {
    os.message(); // android 케이스
  }
}

// iOS 타입
sendMessage({
  iMessage: () => {
    console.log("sending iMessage");
  },
});

// android 타입
sendMessage({
  message: () => {
    console.log("sending message");
  },
});
  • in 연산자는 객체에 해당 프로퍼티가 존재하면 true를 반환하므로, 구조가 다른 API 응답 타입을 구분할 때도 자주 쓰인다.

4. instanceof Narrowing ​

instanceof는 클래스(또는 생성자 함수) 기반 객체를 구분할 때 사용한다.

ts
class ApiResponse {
  data: any;
}

class ErrorResponse {
  message: string;
}

async function handleApiResponse(response: any) {
  if (response instanceof ApiResponse) {
    // 데이터 처리
  } else if (response instanceof ErrorResponse) {
    // 에러 처리
  }
}

const apiResponse = new ApiResponse();
const errorResponse = new ErrorResponse();

handleApiResponse(apiResponse);
handleApiResponse(errorResponse);
  • instanceof는 클래스나 함수로 생성된 객체에만 사용할 수 있다. 인터페이스(interface) 타입에는 적용되지 않는다.

5. 타입 가드 (Type Predicates) ​

사용자 정의 타입 가드 함수는, 복잡한 타입 분기 로직을 함수 형태로 추출해 재사용할 수 있다.

ts
function isErrorResponse(
  response: ApiResponse | ErrorResponse
): response is ErrorResponse {
  return (response as ErrorResponse).message !== undefined;
}

const response = { message: "error.." };

if (isErrorResponse(response)) {
  // 에러케이스
  console.log(response.message);
}
  • 함수의 반환 타입 response is ErrorResponse는 TypeScript에게 직접 타입을 좁히는 규칙을 알려주는 문법이다.

6. 식별 가능한 유니언 타입 (Discriminated Union) ​

공통된 리터럴 속성(type, kind, status 등)을 기준으로 유니언 타입을 안전하게 구분하는 패턴이다. TypeScript의 가장 대표적인 타입 좁히기 기법이다.

ts
type SuccessResponse = {
  type: "success";
  data: any;
};

type ErrorResponseType = {
  type: "error";
  message: string;
};

type ApiResponseType = SuccessResponse | ErrorResponseType;

function handleResponse(response: ApiResponseType) {
  if (response.type === "success") {
    console.log("data:", response.data);
  } else {
    console.log(response.message);
  }
}
  • response.type 값이 success 또는 error로 좁혀지므로, 조건문 안에서 자동 완성과 타입 안전성이 확보된다.