icon
5장 : 고급 타입

타입 가드와 타입 좁히기


타입스크립트는 정적 타입을 통해 코드의 안정성을 높여주지만, 때로는 런타임에 변수의 실제 타입을 확인하고 그에 따라 다르게 동작해야 할 필요가 있습니다. 예를 들어, 어떤 변수가 string 또는 number 타입인 유니온 타입으로 선언되었을 때, 이 변수가 실제로 string인지 number인지에 따라 다른 로직을 수행해야 하는 경우가 대표적입니다.

이러한 상황에서 유용하게 사용되는 것이 바로 타입 가드(Type Guard) 입니다. 타입 가드는 특정 스코프(코드 블록) 내에서 변수의 타입을 더욱 구체적인 타입으로 좁히는(Narrowing) 역할을 합니다. 이를 통해 우리는 런타임에 안전하게 해당 타입의 속성이나 메서드에 접근할 수 있게 됩니다.


타입 좁히기의 개념

타입 좁히기 (Type Narrowing) 는 타입스크립트 컴파일러가 코드의 특정 부분에서 변수의 타입을 원래 선언된 타입보다 더 구체적인 타입으로 인식하는 과정을 말합니다. 이는 주로 조건문(if, else if), 루프, 또는 특정 연산자 사용을 통해 발생합니다. 타입 좁히기가 이루어지면, 해당 코드 블록 내에서 변수는 좁혀진 타입의 속성이나 메서드만을 가지는 것으로 간주되어 타입 안전성이 확보됩니다.

function printId(id: string | number) {
  // 처음에는 'id'의 타입이 'string | number'입니다.
  console.log(`ID: ${id}`);

  // id.toUpperCase(); // Error: 'string | number' 형식에 'toUpperCase' 속성이 없습니다.
                     // number 타입에는 이 메서드가 없기 때문입니다.

  // 타입 가드를 사용하여 타입을 좁힙니다.
  if (typeof id === 'string') {
    // 이 if 블록 안에서 'id'의 타입은 'string'으로 좁혀집니다.
    console.log(`대문자 ID: ${id.toUpperCase()}`);
  } else {
    // 이 else 블록 안에서 'id'의 타입은 'number'로 좁혀집니다.
    console.log(`고정 소수점 ID: ${id.toFixed(2)}`);
  }
}

printId("abc123DEF"); // ID: abc123DEF, 대문자 ID: ABC123DEF
printId(123.456);     // ID: 123.456, 고정 소수점 ID: 123.46

위 예시에서 typeof id === 'string' 조건문이 바로 타입 가드입니다. 이 조건이 참일 경우, 타입스크립트는 if 블록 내에서 id의 타입을 string으로 좁혀줍니다.


주요 타입 가드 종류

타입스크립트에서 기본적으로 제공하는 몇 가지 유용한 타입 가드들이 있습니다.

typeof 연산자

원시 타입(string, number, boolean, symbol, undefined, bigint)에 대한 타입을 확인하는 데 사용됩니다.

function printValue(value: string | number | boolean) {
  if (typeof value === 'string') {
    console.log(`문자열: ${value.length}`);
  } else if (typeof value === 'number') {
    console.log(`숫자: ${value * 2}`);
  } else {
    console.log(`불리언: ${!value}`);
  }
}

printValue("hello");  // 문자열: 5
printValue(100);      // 숫자: 200
printValue(true);     // 불리언: false

instanceof 연산자

클래스의 인스턴스인지 확인하는 데 사용됩니다. 특정 클래스의 인스턴스 또는 해당 클래스를 상속받은 인스턴스에 대해 타입을 좁힐 수 있습니다.

class Bird {
  fly() { console.log('새가 날아갑니다.'); }
  layEggs() { console.log('알을 낳습니다.'); }
}

class Fish {
  swim() { console.log('물고기가 헤엄칩니다.'); }
  layEggs() { console.log('알을 낳습니다.'); }
}

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    animal.fly(); // 'animal'은 Bird 타입으로 좁혀짐
  } else {
    animal.swim(); // 'animal'은 Fish 타입으로 좁혀짐
  }
  animal.layEggs(); // 'Bird'와 'Fish' 모두 layEggs 메서드를 가지므로 안전하게 호출 가능
}

move(new Bird()); // 새가 날아갑니다., 알을 낳습니다.
move(new Fish()); // 물고기가 헤엄칩니다., 알을 낳습니다.

in 연산자

객체에 특정 속성(property)이 존재하는지 확인하는 데 사용됩니다. 유니온 타입으로 묶인 객체 타입에서 특정 속성의 유무로 타입을 구분할 때 유용합니다.

interface Car {
  drive(): void;
}

interface Boat {
  sail(): void;
}

function operateVehicle(vehicle: Car | Boat) {
  if ('drive' in vehicle) {
    vehicle.drive(); // 'vehicle'은 Car 타입으로 좁혀짐
  } else {
    vehicle.sail();  // 'vehicle'은 Boat 타입으로 좁혀짐
  }
}

class MyCar implements Car { drive() { console.log('자동차 운전!'); } }
class MyBoat implements Boat { sail() { console.log('보트 항해!'); } }

operateVehicle(new MyCar());  // 자동차 운전!
operateVehicle(new MyBoat()); // 보트 항해!

동등성 검사 (==, ===, !=, !==)

null 또는 undefined와 같은 리터럴 값과의 동등성 검사를 통해 타입을 좁힐 수 있습니다. 특히 선택적 속성이나 null이 될 수 있는 타입(string | null)에서 유용합니다.

function greetUser(user: { name: string; email?: string | null }) {
  console.log(`안녕하세요, ${user.name}님!`);

  if (user.email) { // user.email이 null 또는 undefined가 아닐 때
    // 이 블록 안에서 user.email의 타입은 string으로 좁혀집니다.
    console.log(`이메일: ${user.email.toLowerCase()}`);
  } else {
    console.log("이메일 정보가 없습니다.");
  }
}

greetUser({ name: "Alice" });             // 안녕하세요, Alice님!, 이메일 정보가 없습니다.
greetUser({ name: "Bob", email: "bob@example.com" }); // 안녕하세요, Bob님!, 이메일: bob@example.com
greetUser({ name: "Charlie", email: null }); // 안녕하세요, Charlie님!, 이메일 정보가 없습니다.

사용자 정의 타입 가드

기본 제공되는 타입 가드로는 충분하지 않거나, 더 복잡한 로직으로 타입을 좁히고 싶을 때 사용자 정의 타입 가드(User-Defined Type Guards) 를 직접 만들 수 있습니다. 사용자 정의 타입 가드 함수는 반환 타입으로 parameterName is Type 형태의 타입 프레디케이트(Type Predicate) 를 가집니다.

interface AdminUser {
  id: number;
  role: 'admin';
  manageUsers(): void;
}

interface GeneralUser {
  id: number;
  role: 'general';
  viewProfile(): void;
}

type User = AdminUser | GeneralUser;

// isAdminUser는 사용자 정의 타입 가드 함수입니다.
// user is AdminUser: 이 함수가 true를 반환하면 user의 타입이 AdminUser임을 보증합니다.
function isAdminUser(user: User): user is AdminUser {
  return (user as AdminUser).role === 'admin';
}

function handleUser(user: User) {
  if (isAdminUser(user)) {
    // 이 블록 안에서 user는 AdminUser 타입으로 좁혀집니다.
    console.log("관리자 권한으로 로그인했습니다.");
    user.manageUsers();
  } else {
    // 이 블록 안에서 user는 GeneralUser 타입으로 좁혀집니다.
    console.log("일반 사용자 권한으로 로그인했습니다.");
    user.viewProfile();
  }
}

const admin: AdminUser = { id: 1, role: 'admin', manageUsers: () => console.log("사용자 관리") };
const general: GeneralUser = { id: 2, role: 'general', viewProfile: () => console.log("프로필 보기") };

handleUser(admin);   // 관리자 권한으로 로그인했습니다., 사용자 관리
handleUser(general); // 일반 사용자 권한으로 로그인했습니다., 프로필 보기

user is AdminUser는 함수가 true를 반환할 때, 첫 번째 매개변수 userAdminUser 타입임을 타입스크립트 컴파일러에게 알려줍니다. 이처럼 사용자 정의 타입 가드를 사용하면 런타임 로직과 타입 검사를 긴밀하게 통합할 수 있습니다.


타입 가드와 타입 좁히기는 타입스크립트의 유니온 타입과 함께 강력한 시너지를 발휘하는 기능입니다. 런타임에 동적으로 변수의 타입을 확인하고 그에 따라 적절한 타입의 멤버에 안전하게 접근함으로써, 런타임 오류를 줄이고 코드의 견고성을 크게 향상시킬 수 있습니다. typeof, instanceof, in 연산자와 더불어 사용자 정의 타입 가드를 적절히 활용하는 것이 중요합니다.