TypeScript 열거형
유니언 열거형과 열거형 멤버 타입
계산되지 않는 상수 열거 멤버의 특수한 부분 집합이 있다. 리터럴 열거형 멤버 리터럴 열거형 멤버는 초기화 값이 존재하지 않거나, 아래 값들로 초기화되는 멤버다.
- 문자 리터럴 (예시.
"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
}
OrderStatus
는 IN_CART
이 0
으로 초기화된 숫자 열겨형이다. 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";
이 한 줄은 실제로 두 가지 매핑을 동시에 만든다.
- 이름 → 값 매핑:
OrderStatus.IN_CART
로 접근하면0
을 반환 OrderStatus["IN_CART"] = 0
- 값 → 이름 매핑:
OrderStatus[0]
으로 접근하면IN_CART
를 반환 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;
이 타입 정의를 분석해보면:
typeof OrderStatus: OrderStatus enum
의 타입을 가져온다.keyof
: 객체의 키들의 유니온 타입을 생성한다.- 결과적으로
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
객체로 작성된 코드를 새로운 JavaScriptenum
문법으로 쉽게 전환(move to the additional syntax)할 수 있다는 장점이 있다. 즉, TypeScript의enum
에 묶여 있지 않고, JavaScript의 발전에 유연하게 대응할 수 있다는 뜻이다.