전체 목록
TypeScriptMedium#90

타입 가드(Type Guard)란 무엇이며, 사용하는 방법들을 설명해주세요.

#TypeScript#타입가드#타입좁히기#타입시스템
힌트

typeof, instanceof, in 연산자와 사용자 정의 타입 가드를 떠올려보세요.

정답 및 해설

타입 가드(Type Guard)란 무엇이며, 사용하는 방법들을 설명해주세요.

타입 가드(Type Guard)는 런타임에 타입을 좁혀(narrow) TypeScript 컴파일러가 특정 코드 블록 내에서 더 구체적인 타입을 추론하게 하는 방법입니다. TypeScript는 유니온 타입(string | number)처럼 여러 타입이 가능한 변수를 다룰 때, 타입 가드를 통해 정확한 타입을 판별하고 타입별로 안전하게 접근할 수 있게 해줍니다.

타입 내로잉(Narrowing)이란?

function processValue(value: string | number) {
  // 여기서 value는 string | number 타입
  value.toUpperCase();  // TS 에러! number에는 toUpperCase가 없음
  value.toFixed(2);     // TS 에러! string에는 toFixed가 없음

  // 타입 가드로 좁히면
  if (typeof value === 'string') {
    // 이 블록 안에서 value는 string 타입
    value.toUpperCase();  // OK
  } else {
    // 이 블록 안에서 value는 number 타입
    value.toFixed(2);  // OK
  }
}

1. typeof 타입 가드

원시 타입(primitive type) 판별에 사용합니다.

// typeof가 반환하는 값: "string", "number", "boolean", "bigint",
//                       "symbol", "undefined", "object", "function"

function formatValue(value: string | number | boolean | null | undefined): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }

  if (typeof value === 'number') {
    return value.toFixed(2);
  }

  if (typeof value === 'boolean') {
    return value ? '예' : '아니오';
  }

  // null과 undefined는 typeof로 완전히 구분 불가
  // typeof null === 'object' (JavaScript의 오래된 버그)
  if (value === null) return '(없음)';
  return '(미정)';  // undefined
}

// 실제 활용
function calculateTotal(price: number, discount?: number): number {
  if (typeof discount === 'undefined') {
    return price;
  }
  return price * (1 - discount);
}

2. instanceof 타입 가드

클래스 인스턴스 타입 판별에 사용합니다.

class NetworkError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

function handleError(error: Error) {
  if (error instanceof NetworkError) {
    // 이 블록 안에서 error는 NetworkError 타입
    console.error(`네트워크 오류 ${error.statusCode}: ${error.message}`);
    if (error.statusCode === 401) {
      redirectToLogin();
    }
  } else if (error instanceof ValidationError) {
    // 이 블록 안에서 error는 ValidationError 타입
    console.error(`검증 오류 - ${error.field}: ${error.message}`);
    showFieldError(error.field, error.message);
  } else {
    console.error(`알 수 없는 오류: ${error.message}`);
  }
}

// Date, Array 등 내장 클래스에도 활용
function processInput(input: string | Date) {
  if (input instanceof Date) {
    return input.toISOString();  // input은 Date 타입
  }
  return new Date(input).toISOString();  // input은 string 타입
}

3. in 연산자 타입 가드

객체가 특정 속성을 가지고 있는지 확인합니다.

interface Dog {
  name: string;
  bark(): void;
}

interface Cat {
  name: string;
  meow(): void;
}

interface Bird {
  name: string;
  fly(): void;
  sing(): void;
}

type Animal = Dog | Cat | Bird;

function makeSound(animal: Animal) {
  if ('bark' in animal) {
    // animal은 Dog 타입
    animal.bark();
  } else if ('meow' in animal) {
    // animal은 Cat 타입
    animal.meow();
  } else {
    // animal은 Bird 타입
    animal.fly();
    animal.sing();
  }
}

// 선택적 속성 확인에도 활용
interface AdminUser {
  name: string;
  adminLevel: number;
  permissions: string[];
}

interface RegularUser {
  name: string;
}

type User = AdminUser | RegularUser;

function showDashboard(user: User) {
  if ('adminLevel' in user) {
    // user는 AdminUser 타입
    console.log(`관리자 레벨: ${user.adminLevel}`);
    console.log(`권한: ${user.permissions.join(', ')}`);
  } else {
    // user는 RegularUser 타입
    console.log('일반 사용자');
  }
}

4. 사용자 정의 타입 가드 (Type Predicate)

반환 타입에 arg is Type 형식을 사용하여 타입 가드 함수를 직접 만듭니다.

// 기본 문법: function fn(arg: T): arg is SpecificType
interface Cat { type: 'cat'; meow(): void }
interface Dog { type: 'dog'; bark(): void }
type Animal = Cat | Dog;

// 사용자 정의 타입 가드 함수
function isCat(animal: Animal): animal is Cat {
  return animal.type === 'cat';
}

function isDog(animal: Animal): animal is Dog {
  return animal.type === 'dog';
}

function handleAnimal(animal: Animal) {
  if (isCat(animal)) {
    animal.meow();  // animal은 Cat 타입으로 좁혀짐
  } else {
    animal.bark();  // animal은 Dog 타입으로 좁혀짐
  }
}

// 실용적인 예: null/undefined 체크
function isNonNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

const values: (number | null | undefined)[] = [1, null, 2, undefined, 3];
const nonNullValues = values.filter(isNonNull);  // number[] 타입!
// isNonNull 없이 filter하면 (number | null | undefined)[] 타입

// API 응답 검증
interface SuccessResponse {
  success: true;
  data: unknown;
}

interface ErrorResponse {
  success: false;
  error: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function isSuccess(response: ApiResponse): response is SuccessResponse {
  return response.success === true;
}

async function fetchData() {
  const response: ApiResponse = await callApi();
  if (isSuccess(response)) {
    processData(response.data);  // response.data 접근 가능
  } else {
    showError(response.error);   // response.error 접근 가능
  }
}

5. Discriminated Union (판별 유니온)

공통된 리터럴 타입 필드(태그)를 이용해 유니온 타입을 구분합니다.

// 도형 타입 - 각각 고유한 kind 값을 가짐
interface Circle {
  kind: 'circle';
  radius: number;
}

interface Rectangle {
  kind: 'rectangle';
  width: number;
  height: number;
}

interface Triangle {
  kind: 'triangle';
  base: number;
  height: number;
}

type Shape = Circle | Rectangle | Triangle;

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      // shape는 Circle 타입
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      // shape는 Rectangle 타입
      return shape.width * shape.height;
    case 'triangle':
      // shape는 Triangle 타입
      return (shape.base * shape.height) / 2;
    default:
      // 모든 케이스를 처리했는지 컴파일 타임에 검증 (Exhaustiveness Check)
      const _exhaustiveCheck: never = shape;
      throw new Error(`처리되지 않은 shape: ${_exhaustiveCheck}`);
  }
}

// Redux Action 패턴에서 활용
type AppAction =
  | { type: 'INCREMENT'; amount: number }
  | { type: 'DECREMENT'; amount: number }
  | { type: 'RESET' }
  | { type: 'SET_VALUE'; value: number };

function reducer(state: number, action: AppAction): number {
  switch (action.type) {
    case 'INCREMENT': return state + action.amount;   // action.amount: number
    case 'DECREMENT': return state - action.amount;   // action.amount: number
    case 'RESET': return 0;
    case 'SET_VALUE': return action.value;             // action.value: number
  }
}

6. Nullish Check (null/undefined 체크)

interface User {
  name: string;
  email?: string;  // optional
  address: string | null;
}

function processUser(user: User) {
  // optional 속성 접근 시
  if (user.email !== undefined) {
    // user.email은 string 타입
    sendEmail(user.email.toLowerCase());
  }

  // null 체크
  if (user.address !== null) {
    // user.address는 string 타입
    console.log(user.address.trim());
  }

  // null과 undefined 동시 체크 (nullish check)
  if (user.email != null) {  // != null은 null과 undefined 둘 다 체크
    user.email.toLowerCase();
  }

  // 옵셔널 체이닝으로 간소화 (타입 가드는 아니지만 유용)
  const lowerEmail = user.email?.toLowerCase();  // string | undefined
}

타입 가드 조합

// 여러 타입 가드를 조합한 복잡한 케이스
type Input = string | number | boolean | null | undefined | object;

function processInput(input: Input) {
  // null/undefined 먼저 처리
  if (input == null) {
    return 'empty';
  }

  // input은 이제 string | number | boolean | object
  if (typeof input === 'object') {
    if (Array.isArray(input)) {
      return `배열 (${input.length}개)`;
    }
    return `객체: ${JSON.stringify(input)}`;
  }

  if (typeof input === 'string') {
    return input.trim();
  }

  if (typeof input === 'number') {
    return input.toFixed(2);
  }

  // input은 boolean
  return input ? 'true' : 'false';
}

정리

타입 가드 방법사용 시기예시
typeof원시 타입 구분typeof x === 'string'
instanceof클래스 인스턴스 구분x instanceof Error
in 연산자속성 존재 여부로 구분'swim' in animal
사용자 정의 (Type Predicate)복잡한 조건으로 구분function isCat(a): a is Cat
Discriminated Union태그 필드로 구분switch(shape.kind)
Nullish Checknull/undefined 제거if (x != null)
Array.isArray배열 여부 구분Array.isArray(x)

핵심: 타입 가드는 TypeScript에게 "이 조건을 통과하면 이 타입이 확실하다"고 알려주는 방법입니다. 런타임 안전성과 컴파일 타임 타입 안전성을 동시에 달성합니다.