<Demopeu/>

타입 좁히기(Type-Narrowing)

타입 좁히기(Type-Narrowing)

referencetypescripttype-narrowingtype-guard

특정 상황에서 타입의 범위를 더 좁고 명확한 타입으로 한정하는 과정

타입 좁히기는 조건문 등을 이용해 넓은 타입에서 좁은 타입으로 변환하는 과정이다.

조건문과 함께 사용해 타입을 좁히는 표현들을 타입 가드(Type Guard)라고 한다.

아래 3가지가 대표적인 타입 가드다.

🎯 실무에서 이런 상황에 유용하다

  • API 응답 처리: 서버에서 받은 데이터가 성공/실패에 따라 다른 타입일 때
  • 폼 데이터 검증: 사용자 입력값이 문자열인지 숫자인지 확인할 때
  • 이벤트 핸들링: 다양한 이벤트 타입(MouseEvent, KeyboardEvent 등)을 구분할 때
  • 유니온 타입 처리: 여러 타입을 받을 수 있는 함수에서 각 타입별로 다른 로직을 실행할 때

💡 사용 예시

1. typeof를 이용한 타입 좁히기

function handleValue(value: string | number) {
  if (typeof value === "string") {
    // value는 이제 string 타입으로 좁혀짐
    console.log(value.toUpperCase());
  } else {
    // value는 이제 number 타입으로 좁혀짐
    console.log(value.toFixed(2));
  }
}

만약 JS 기본 클래스들도 typeof로 하면 어떨까?

function handleClass(value: string | number | Date | null) {
  if (typeof value === "object") {
    console.log(value.getTime()); // ❌ Error: value는 'null'일 수 있습니다.
  }
}

typeof 연산자는 null 값에 해도 "object"를 반환하기 때문에 위험 할 수 있다.

2. instanceof를 이용한 타입 좁히기

function handleClass(value: string | number | Date | null) {
  if (value instanceof Date) {
    console.log(value.getTime());
  }
}

이때 instanceof를 사용하면 된다. 하지만, instanceof는 내장 클래스나 직접 만든 클래스에만 사용할 수 있다. 즉, 직접 만든 타입을 체크할 수 없는 단점이 있다.

3. in을 이용한 타입 좁히기

type Person = {
  name: string;
  age: number;
};

function processPerson(person: Person | null) {
  if (person && "age" in person) {
    // person은 이제 Person 타입으로 좁혀짐
    console.log(person.age);
  }
}

in은 객체가 특정 프로퍼티를 가지고 있는지 확인하는 연산자이다. 대신 null일 때는 안전하게 처리해야 하기 때문에 앞에 null 체크를 해주는 것이 좋다.

💡 서로소 유니온 타입을 이용해 타입 좁히기

type User = {
  name: string;
  age: number;
};
type Admin = {
  name: string;
  role: string;
};
type Person = User | Admin;

function processPerson(person: Person) {
  if ("age" in person) {
    console.log(person.name, person.age);
  } else {
    console.log(person.name, person.role);
  }
}

이런식으로 하면 코드를 짠 사람은 이해하지만, 코드를 읽는 사람은 타입까지 읽어야해서 가독성이 떨어진다. 이때, 서로소 유니온 타입을 사용한다면 훨씬 가독성 있는 코드를 짤 수 있다.

type User = {
  type: "user";
  name: string;
  age: number;
};
type Admin = {
  type: "admin";
  role: string;
};
type Person = User | Admin;

function processPerson(person: Person) {
  if (person.type === "user") { // person은 User 타입으로 좁혀짐
    console.log(person.name, person.age);
  } else { // person은 Admin 타입으로 좁혀짐
    console.log(person.name, person.role);
  }
}

함수만 봐도 딱 알 수 있게 된다.

🎯 실무에서 사용해볼만한 코드

type AsyncResponse = {
  status: 'success' | 'error' | 'loading';
  error?: string;
  data?: string;
};

function processResponse(response: AsyncResponse) {
  switch (response.status) {
    case 'success':
      console.log(response.data); // data가 있는지 모름. 따라서 ?나 !를 사용해야만함.
      break;
    case 'error':
      console.log(response.error); // error가 있는지 모름. 따라서 ?나 !를 사용해야만함.
      break;
    case 'loading':
      console.log('Loading...');
      break;
  }
}

위의 코드의 타입을 좀 더 잘게 쪼갠다면,

type SuccessResponse = {
  status: 'success';
  data: string;
};
type ErrorResponse = {
  status: 'error';
  error: string;
};
type LoadingResponse = {
  status: 'loading';
};
type AsyncResponse = SuccessResponse | ErrorResponse | LoadingResponse;

function processResponse(response: AsyncResponse) {
  switch (response.status) {
    case 'success':
      console.log(response.data);
      break;
    case 'error':
      console.log(response.error);
      break;
    case 'loading':
      console.log('Loading...');
      break;
  }
}