개발

TypeScript 열거형

belljun 2025. 7. 16. 00:03

유니언 열거형과 열거형 멤버 타입

계산되지 않는 상수 열거 멤버의 특수한 부분 집합이 있다. 리터럴 열거형 멤버 리터럴 열거형 멤버는 초기화 값이 존재하지 않거나, 아래 값들로 초기화되는 멤버다.

  • 문자 리터럴 (예시. "foo", "bar, "baz")
  • 숫자 리터럴 (예시. 1, 100)
  • 숫자 리터럴에 단항 연산자 - 가 적용된 경우 (e.g. -1, -100)

 

열거형의 모든 멤버가 리터럴 열거형 값을 가지면 특별한 의미로 쓰이게 된다.

 

첫째로 열거형 멤버를 타입처럼 사용할 수 있다. 예를 들어 특정 멤버는 오직 열거형 멤버의 값만 가지게 할 수 있다.

enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}

let c: Circle = {
    kind: ShapeKind.Square, // 오류! 'ShapeKind.Circle' 타입에 'ShapeKind.Square' 타입을 할당할 수 없습니다. Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
    radius: 100,
};

 

또 다른 점은 열거형 타입 자체가 효율적으로 각각의 열거형 멤버의 유니언이 된다는 점이다. 유니언 타입 열거형을 사용하면 타입 시스템이 열거형 자체에 존재하는 정확한 값의 집합을 알고 있다는 사실을 활용할 수 있다는 점만 알아두면 된다. 이 때문에 TypeScript는 값을 잘못 비교하는 어리석은 버그를 잡을 수 있다. 예를 들어:

enum E {
    Foo,
    Bar,
}

function f(x: E) {
    if (x !== E.Foo || x !== E.Bar) {
    // This comparison appears to be unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.
    // ~~~~~~~~~~~
    // 에러! E 타입은 Foo, Bar 둘 중 하나이기 때문에 이 조건은 항상 true를 반환합니다.
    }
}

 

런타임에서의 열거형

열거형은 런타임에 존재하는 실제 객체다. 예를 들어 아래와 같은 열거형은

enum E {
    X,
    Y,
    Z,
}

 

실제로 아래와 같이 함수로 전달될 수 있다.

function f(obj: { X: number }) {
    return obj.X;
}

// E가 X라는 숫자 프로퍼티를 가지고 있기 때문에 동작하는 코드다.
f(E);

 

컴파일 시점에서 열거형

열거형이 런타임에 존재하는 실제 객체라고 할지라도, keyof 키워드는 일반적인 객체에서 기대하는 동작과 다르게 동작한다. 대신, keyof typeof 를 사용하면 모든 열거형의 키를 문자열로 나타내는 타입을 가져온다.

enum LogLevel {
    ERROR,
    WARN,
    INFO,
    DEBUG,
}

/**
* 이것은 아래와 동일합니다. :
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;

 

역 매핑

숫자 열거형 멤버는 멤버의 프로퍼티 이름을 가진 객체를 생성하는 것 외에도 열거형 값에서 열거형 이름으로 역 매핑을 받는다. 예를 들어 아래의 예제에서:

enum Enum {
    A,
}

let a = Enum.A;
let nameOfA = Enum[a]; // "A"

 

TypeScript는 아래와 같은 JavaScript 코드로 컴파일할 것이다.

"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));

let a = Enum.A;
let nameOfA = Enum[a]; // "A"

이렇게 생성된 코드에서, 열거형은 정방향 (name -> value) 매핑과 역방향 (value -> name) 매핑 두 정보를 모두 저장하는 객체로 컴파일된다. 다른 열거형 멤버 참조는 항상 프로퍼티 접근으로 노출되며 인라인 되지 않는다.

 

문자열 열거형은 역 매핑을 생성하지 않는다는 것을 명심하자.

 

열거형 예시

// order.ts
/**
* 주문의 현재 상태를 나타내는 열거형
* 주문 생성부터 배송 완료까지의 프로세스를 추적
*/
export enum OrderStatus {
    /** 장바구니에 담긴 상태 */
    IN_CART = 0,
    /** 결제 대기 중 */
    PENDING_PAYMENT = 1,
    /** 결제 완료 */
    PAID = 2,
    /** 상품 준비 중 */
    PREPARING = 3,
    /** 배송 시작 */
    SHIPPED = 4,
    /** 배송 완료 */
    DELIVERED = 5,
    /** 주문 취소 */
    CANCELLED = 6,
    /** 환불 완료 */
    REFUNDED = 7
}

OrderStatusIN_CART0으로 초기화된 숫자 열겨형이다. IN_CART 이후 뒤따르는 멤버들은 자동으로 증가된 값을 가진다. 명시적으로 초기화하지 않는 경우에도 가장 먼저 오는 값은 0을 가진다.

 

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrderStatus = void 0;

/**
* 주문의 현재 상태를 나타내는 열거형
* 주문 생성부터 배송 완료까지의 프로세스를 추적
*/
var OrderStatus;
(function (OrderStatus) {
/** 장바구니에 담긴 상태 */
OrderStatus[OrderStatus["IN_CART"] = 0] = "IN_CART";
/** 결제 대기 중 */
OrderStatus[OrderStatus["PENDING_PAYMENT"] = 1] = "PENDING_PAYMENT";
/** 결제 완료 */
OrderStatus[OrderStatus["PAID"] = 2] = "PAID";
/** 상품 준비 중 */
OrderStatus[OrderStatus["PREPARING"] = 3] = "PREPARING";
/** 배송 시작 */
OrderStatus[OrderStatus["SHIPPED"] = 4] = "SHIPPED";
/** 배송 완료 */
OrderStatus[OrderStatus["DELIVERED"] = 5] = "DELIVERED";
/** 주문 취소 */
OrderStatus[OrderStatus["CANCELLED"] = 6] = "CANCELLED";
/** 환불 완료 */
OrderStatus[OrderStatus["REFUNDED"] = 7] = "REFUNDED";
})(OrderStatus || (exports.OrderStatus = OrderStatus = {}));

npx tsc order.ts --target ES5 명령을 통해 JavaScript로 컴파일된 파일은 위와 같다.

 

컴파일된 코드를 보면, TypeScript enum은 아래와 같은 양방향 매핑(bidirectional mapping)을 생성한다.

 

OrderStatus[OrderStatus["IN_CART"] = 0] = "IN_CART";

이 한 줄은 실제로 두 가지 매핑을 동시에 만든다.

  1. 이름 → 값 매핑: OrderStatus.IN_CART로 접근하면 0을 반환
  2. OrderStatus["IN_CART"] = 0
  3. 값 → 이름 매핑: OrderStatus[0]으로 접근하면 IN_CART를 반환
  4. OrderStatus[0] = "IN_CART"

 

숫자 열거형에서는 값 -> 이름 매핑이 존재하지만 문자열 매핑에서는 값 -> 이름 매핑이 없다. 문자열이 고유하지 않을 수 있기 때문이다.

 

{
  '0': 'IN_CART',
  '1': 'PENDING_PAYMENT',
  '2': 'PAID',
  '3': 'PREPARING',
  '4': 'SHIPPED',
  '5': 'DELIVERED',
  '6': 'CANCELLED',
  '7': 'REFUNDED',
  IN_CART: 0,
  PENDING_PAYMENT: 1,
  PAID: 2,
  PREPARING: 3,
  SHIPPED: 4,
  DELIVERED: 5,
  CANCELLED: 6,
  REFUNDED: 7
}

따라서 숫자 열거형은 이름 → 값 매핑과 값 → 이름 매핑을 수행하기 때문에 OrderStatus는 위와ㅌ 같이 구성된다.

 

이에 Object.keys(OrderStatus)를 통해 OrderStatus의 키를 조회하면 아래와 같다.

const allKeys = Object.keys(OrderStatus);

console.log('모든 키:', allKeys);
모든 키: [
  '0',         '1',
  '2',         '3',
  '4',         '5',
  '6',         '7',
  'IN_CART',   'PENDING_PAYMENT',
  'PAID',      'PREPARING',
  'SHIPPED',   'DELIVERED',
  'CANCELLED', 'REFUNDED'
]

 

열거형 예시의 키

/** OrderStatus 열거형의 키를 문자열로 표현하는 타입 (예: "PAID", "SHIPPED") */
export type OrderStatusString = keyof typeof OrderStatus;

이 타입 정의를 분석해보면:

  1. typeof OrderStatus: OrderStatus enum의 타입을 가져온다.
  2. keyof: 객체의 키들의 유니온 타입을 생성한다.
  3. 결과적으로 OrderStatusString은 아래와 같은 타입이 된다.

 

type OrderStatusString = "IN_CART" | "PENDING_PAYMENT" | "PAID" | "PREPARING" | "SHIPPED" | "DELIVERED" | "CANCELLED" | "REFUNDED"

위를 통해 알 수 있는 것은 런타임에는 숫자 키와 문자열 키가 모두 포함되지만 TypeScript의 타입 시스템에서는 OrderStatusString이 문자열 키만 포함되는 것을 확인할 수 있다.

 

Objects vs. Enums (객체 vs. Enum)

  • TypeScript Enum: const enum EDirection { ... } 형태로 정의되며, EDirection.Up처럼 사용하면 그 값이 0임을 명확히 보여준다.
  • JavaScript 객체 + as const: const ODirection = { ... } as const; 형태로 정의된다. 여기서 as const는 해당 객체가 "상수처럼" 취급되어 속성들이 리터럴 타입으로 추론되도록 한다. ODirection.Up처럼 사용하면 해당 속성이 값 0을 가리킨다는 것을 보여준다.

 

주요 차이점 및 사용 예시

Enum 사용

function walk(dir: EDirection) {}
walk(EDirection.Left);

EDirection 타입을 직접 함수의 매개변수로 사용하여 명확하게 방향을 나타낸다.

 

as const 객체 사용

type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
run(ODirection.Right);

객체를 타입으로 사용하려면 type Direction = typeof ODirection[keyof typeof ODirection];와 같이 한 줄의 추가적인 타입 정의가 필요하다. 이 코드는 ODirection 객체의 모든 속성 값(0, 1, 2, 3)을 유니온 타입 Direction으로 추출한다.

 

as const 객체를 선호하는가?

  • JavaScript와의 정렬 (Alignment with JavaScript): as const를 사용한 객체는 순수한 JavaScript 문법에 가깝다. 즉, TypeScript에 특화된 enum 문법과 달리, 일반적인 JavaScript에서도 이해하고 사용할 수 있는 방식이라는 의미다.
  • 향후 전환 용이성 (Future Transition): 만약 언젠가 JavaScript 자체에 enum 기능이 추가된다면, 현재 as const 객체로 작성된 코드를 새로운 JavaScript enum 문법으로 쉽게 전환(move to the additional syntax)할 수 있다는 장점이 있다. 즉, TypeScript의 enum에 묶여 있지 않고, JavaScript의 발전에 유연하게 대응할 수 있다는 뜻이다.