TypeScript enum은 이름 붙은 상수의 집합입니다. enum Direction { Up, Down, Left, Right }는 타입 Direction과, Direction.Up처럼 멤버에 접근하는 런타임 객체를 함께 만듭니다. 값을 주지 않으면 멤버는 0부터 번호가 매겨지고, 문자열 enum은 각 멤버에 읽기 쉬운 문자열을 줍니다.
enum은 단순한 타입이 아닌 몇 안 되는 TypeScript 기능 중 하나입니다. 코드를 컴파일하면 enum은 실제 JavaScript 객체가 됩니다.
숫자 enum
초기값이 없으면 멤버는 0, 1, 2 순으로 값을 받습니다. 첫 멤버에 숫자를 주면 나머지는 거기서부터 이어집니다. 모든 값을 명시적으로 지정할 수도 있는데, 숫자를 데이터베이스에 저장하거나 네트워크로 보낸다면 이 방법이 안전합니다.
값이 프로그램 밖으로 나가지 않는다면 자동 번호 매기기에 기대도 괜찮습니다. 멤버 순서가 바뀔 수 있고 숫자가 어딘가에 저장된다면, 중간에 멤버를 하나 끼워 넣는 순간 그 뒤의 모든 번호가 조용히 바뀝니다.
enum은 무엇으로 컴파일되나
타입은 지워지지만 enum은 지워지지 않습니다. 숫자 enum과 문자열 enum에 대해 TypeScript가 출력하는 JavaScript는 다음과 같습니다.
enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
Direction["Up"] = 0은 0을 반환하므로 같은 문장에서 Direction[0] = "Up"도 설정됩니다. 따라서 숫자 enum은 이름에서 숫자로, 숫자에서 다시 이름으로 양방향 매핑됩니다. 이것이 역방향 매핑 (reverse mapping)입니다. 문자열 enum은 이름에서 값으로만 매핑합니다.
출력된 Direction 객체에는 키가 여덟 개 있습니다. 이름 네 개와 숫자 네 개입니다. 순회를 시작하는 순간 이 점이 중요해집니다.
문자열 enum
문자열 enum의 각 멤버에는 명시적인 문자열 값이 필요합니다. 값이 로그, JSON, 데이터베이스에 그대로 나타나므로 숫자보다 디버깅하기 쉽습니다.
문자열 enum은 한 가지 면에서 명목적(nominal)이어서 사람들을 놀라게 합니다. 텍스트가 멤버의 값과 같더라도 일반 문자열은 대입할 수 없습니다.
index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?
(메시지의 제안은 컴파일러의 추측이며 여기서는 틀렸습니다. 올바른 수정은 Status.Active입니다.) 반대 방향으로는 Status 값을 string이 필요한 모든 곳에 쓸 수 있습니다. JSON이나 폼에서 값이 문자열로 들어온다면 아래 "enum 값인지 확인하기" 절과 같은 검사로 변환하세요.
enum을 타입으로 쓰기
enum 이름은 멤버들을 값으로 가지는 타입입니다. switch와 함께 쓰면, 함수가 값을 반환해야 할 때 모든 멤버를 처리했는지 TypeScript가 검사합니다.
Shape에 멤버를 추가하고 case를 추가하지 않으면 sides는 TS2366 Function lacks ending return statement and return type does not include 'undefined'.로 컴파일되지 않습니다. never를 이용한 더 엄격한 전수 검사는 switch 페이지에서 볼 수 있습니다.
마지막 줄들은 숫자 enum의 실제 약점을 보여 줍니다. 어떤 멤버와도 맞지 않는 숫자 리터럴 const level: Level = 99는 컴파일 오류(TS2322)지만, number 타입의 값은 무엇이든 받아들이므로 57이 통과합니다. 문자열 enum에는 이런 구멍이 없습니다.
enum 순회하기
enum은 런타임에 객체이므로 Object.keys, Object.values, Object.entries가 동작합니다. 문자열 enum에서는 정확히 멤버만 반환합니다. 숫자 enum에서는 역방향 매핑 항목도 반환하므로 걸러 내야 합니다.
변수 타입을 "enum 멤버 이름 중 하나"로 지정하려면 keyof typeof Direction을 쓰세요. 이는 유니언 "Up" | "Down" | "Left" | "Right"입니다. 그러면 Direction[name]으로 완전한 타입 안전성을 유지하며 값을 조회할 수 있습니다.
문자열 enum에는 역방향 매핑이 없으므로 값에서 멤버 이름을 얻으려면 항목을 검색합니다. Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0]은 "Active"이고, 그 값을 가진 멤버가 없으면 undefined입니다.
enum 값인지 확인하기
프로그램 외부에서 온 데이터는 일반 string이나 number입니다. 타입 가드로 enum의 값들과 비교해 enum 타입으로 좁힙니다.
신뢰할 수 없는 입력에 raw as Status는 피하세요. 단언은 컴파일되지만 런타임에는 아무것도 검사하지 않으므로, "DELETED"가 올바른 Status 타입인 채로 프로그램 곳곳을 돌아다니게 됩니다.
const enum
const enum은 컴파일러에게 enum을 삭제하고 사용한 곳마다 각 멤버의 값을 적어 넣으라고 요청합니다. 런타임에 객체가 없으므로 순회도 역방향 매핑도 할 수 없습니다.
const enum은 몇 바이트와 프로퍼티 조회 한 번을 아껴 주지만, 컴파일러가 그 enum을 쓰는 모든 파일을 컴파일할 때 enum 선언을 볼 수 있어야 합니다. Babel과 swc처럼 파일을 하나씩 트랜스파일하는 도구는 다른 파일에 선언된 const enum을 볼 수 없습니다. Node의 타입 제거는 다른 enum과 마찬가지로 const enum도 거부합니다. 또한 isolatedModules나 verbatimModuleSyntax를 켜면 선언 파일의 const enum을 쓸 때 TypeScript가 TS2748 오류를 냅니다. 대부분의 애플리케이션 코드에는 const enum이 필요 없습니다.
enum과 유니언 타입과 as const 객체 비교
고정된 값의 집합을 정의하는 흔한 방법은 세 가지입니다.
enum | 리터럴 유니언 | as const 객체 | |
|---|---|---|---|
| 런타임에 존재 | 예, 객체로 | 아니요 | 예, 일반 객체로 |
| 값 순회 | Object.values(숫자 enum은 필터링) | 불가, 순회할 것이 없음 | Object.values |
일반 문자열 "red"를 받음 | 아니요(문자열 enum) | 예 | 예 |
이름으로 접근 X.Red | 예 | 아니요 | 예 |
| 역방향 매핑 | 숫자 enum만 | 아니요 | 아니요 |
| Node 타입 제거로 실행 | 아니요 | 예 | 예 |
erasableSyntaxOnly에서 허용 | 아니요 | 예 | 예 |
| 추가로 배울 문법 | enum 규칙, const enum | 없음 | typeof 패턴 |
요즘 많은 팀이 기본으로 문자열 리터럴 유니언을 쓰고, 런타임에 값이 필요할 때(순회하거나 드롭다운을 만들 때) as const 객체로 바꿉니다. 이유는 이렇습니다. 유니언은 순수한 타입이라 출력에서 사라지고, JSON과 API가 전달하는 일반 문자열을 받으며, enum은 일상적인 TypeScript에서 유일하게 "JavaScript에 지울 수 있는 타입을 더한 것"이 아닌 요소입니다.
마지막 이유는 이제 실제로 문제가 됩니다. Node는 타입을 제거해서 .ts 파일을 직접 실행하는데, enum은 제거할 수 있는 대상이 아닙니다.
node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
Node의 --experimental-transform-types 플래그를 쓰면 enum이 실행되고, 컴파일러 옵션 erasableSyntaxOnly는 모든 enum을 TS1294 오류 This syntax is not allowed when 'erasableSyntaxOnly' is enabled.로 보고하므로 프로젝트에서 enum을 미리 금지할 수 있습니다. 타입 제거가 동작하는 방식은 TypeScript 실행하기를 참고하세요. 그렇다고 enum이 잘못된 것은 아닙니다. tsc나 번들러로 컴파일한 코드에서는 enum이 문제없이 실행되며, 이미 enum을 쓰는 코드베이스라면 변환해서 얻는 이득은 크지 않습니다.
자주 묻는 질문
TypeScript에서 enum이란 무엇인가요?
타입이면서 런타임 객체이기도 한, 이름 붙은 상수의 집합입니다. enum Direction { Up, Down }을 선언하면 Direction.Up이라고 쓸 수 있고 Direction을 매개변수 타입으로 쓸 수 있습니다. 대부분의 TypeScript 기능과 달리 enum은 지워지지 않고, 런타임에 존재하는 JavaScript 객체로 컴파일됩니다.
TypeScript에서 enum을 순회하려면 어떻게 하나요?
문자열 enum이라면 Object.values(MyEnum)으로 값을, Object.keys(MyEnum)으로 이름을 얻습니다. 숫자 enum에는 역방향 매핑 항목("0": "Up")도 들어 있으므로 걸러 내야 합니다. Object.keys(Direction).filter((k) => isNaN(Number(k)))는 이름만 돌려줍니다. const enum은 런타임에 존재하지 않으므로 순회할 수 없습니다.
TypeScript에서 문자열을 enum 값으로 변환하려면 어떻게 하나요?
타입 가드에서 문자열을 enum의 값들과 비교합니다: function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }. 검사를 통과하면 s의 타입은 Status입니다. 그냥 s as Status라고 쓰면 컴파일은 되지만 런타임에는 아무것도 검사하지 않습니다.
TypeScript에서 enum과 유니언 타입 중 무엇을 써야 하나요?
많은 팀이 문자열 리터럴 유니언(type Status = "active" | "inactive")을, 런타임에 값도 필요하면 as const 객체를 선호합니다. 유니언은 완전히 지워지고, Node의 내장 타입 제거(type stripping)와 erasableSyntaxOnly 옵션에서도 동작하며, "active" 같은 일반 문자열을 받습니다. enum도 괜찮습니다. 특히 이미 enum을 쓰고 있는 코드베이스라면 더 그렇습니다.
enum과 const enum의 차이는 무엇인가요?
일반 enum은 런타임에 순회하고 조회할 수 있는 객체로 컴파일됩니다. const enum은 컴파일 중에 제거되고 사용한 곳마다 값으로 바뀌므로(Size.Large는 2가 됩니다) 런타임 비용은 없지만 순회할 수 없고, 파일을 하나씩 컴파일하는 도구에서는 사용이 제한됩니다.