개발 중 type을 명확히 하고,
코드의 예측 가능성을 높이기 위해 enum을 자주 사용한다.
TypeScript enum에 대해 자세히 알아보자.
개념
enum(열거형)은 TypeScript가 제공하는 기능(문법) 중 하나다.
enum으로 이름이 있는 상수들의 집합을 정의할 수 있다.
enum을 사용하면 코드의 의도를 전달하기 용이하고,
여러 값들을 하나로 묶어 그 집합의 의미를 전달하기 쉽다.
enum에는 숫자 enum과 문자 enum이 있다.
숫자 열거형(Numeric enums)
enum은 "enum" 키워를 사용해 정의한다.
enum Direction {
Up = 1,
Down,
Left,
Right,
}
위 코드에서 Up이 1로 초기화됐다.
그 외 값은 초기화하지 않아도, 자동으로 1씩 증가된 값을 갖는다.
Down = 2, Left = 3, Right = 4 가 된다.
전부 초기화하지 않을 수도 있는데,
그렇게 되면 첫번째 값이 0이 되고, 1씩 증가한다.
enum Direction {
Up,
Down,
Left,
Right,
}
Up = 0, Down = 1, Left = 2, Right = 3 이다.
이렇게 자동 증가하는 enum 기능은
멤버 값 자체는 중요하지 않으나,
각 값이 다른 값과 구별되어야 할 때 유용하다.
문자열 enum (Strign enum)
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
문자열 enum은 숫자 enum과 유사하지만, 런타임에서 enum의 동작이 약간 다르다. (아래서 더 살펴보자)
문자열 enum에서 각 멤버들은 문자열 리터럴 또는
다른 문자열 enum의 멤버로 상수 초기화 해야 한다.
문자열 enum은 숫자 enum처럼 자동 증가하는 기능은 없다.
기술적으로 enum에 숫자와 문자를 섞어서 사용할 수 있지만, 권장되지 않는다.
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
enum 사용법
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
respond("Princess Caroline", UserResponse.Yes);
enum의 이름을 사용해 타입을 선언할 수 있다.
enum 프로퍼티로 모든 멤버에 접근할 수 있다.
계산된 멤버와 상수 멤버 (Computed and constant members)
enum의 멤버는 상수이거나 계산된 값일 수 있다.
아래 경우들은 상수로 간주한다.
// E.X는 상수입니다:
enum E {
X,
}
enum의 첫 번째 데이터이고, 초기화 값이 없어 0으로 할당된 경우
// 'E1' 과 'E2' 의 모든 열거형 멤버는 상수입니다.
enum E1 {
X,
Y,
Z,
}
enum E2 {
A = 1,
B,
C,
}
초기화 값이 없으며, 숫자 상수로 초기화된 enum 멤버 뒤에 오는 경우.
앞에 나온 값에 1씩 증가한 값을 상수로 갖는다.
enum 멤버는 상수 enum 표현식으로 초기화된다.
상수 enum 표현식은 컴파일 시 알아낼 수 있는 TypesCript 표현식의 일부다.
아래의 경우들을 상수 enum 표현식이라고 한다.
- 리터럴 enum 표현식 (문자 리터럴 또는 숫자 리터럴)
- 이전에 정의된 다른 상수 enum에 대한 참도
- 괄호로 묶인 상수 enum 표현식
- 상수 enum 표현식에 단항 연산자 +, -, ~ 를 사용한 경우
- 상수 enum 표현식을 이중 연산자 +, -, *, /, %, <<, >>, >>>, &, |, ^ 의 피연산자로 사용할 경우
상수 enum 표현식 값이 NaN이거나 Infinity 이면 컴파일 시점에 에러 발생한다.
이외 다른 모든 경우 enum 멤버는 계산된 것으로 간주한다.
enum FileAccess {
// 상수 멤버
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 계산된 멤버
G = "123".length,
}
유니언 enum과 enum 멤버 타입 (Union enums and enum member types)
enum의 모든 멤버가 리터럴 enum 값을 가지면 2가지 특수한 의미가 부여된다.
1. enum 멤버를 타입처럼 사용한다.
예를 들어, 특정 멤버는 오직 enum 멤버의 값만 가지게 할 수 있다.
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,
};
2. enum 타입 자체가 각각의 enum 멤버의 유니언이 된다.
유니언 타입 enum을 사용하면
타입 시스템이 enum 자체에 존재하는 정확한 값의 집합을 알고 있다는 점을 활용할 수 있다.
이를 통해 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 (Enums at runtime)
enum은 런타임에 실제 존재하는 객체다.
예를 들어 아래와 같이 enum E는 함수로 전달될 수 있다.
enum E {
X,
Y,
Z,
}
function f(obj: { X: number }) {
return obj.X;
}
// E가 X라는 숫자 프로퍼티를 가지고 있기 때문에 동작하는 코드입니다.
f(E);
컴파일 시점에서의 enum (Enums at complie time)
enum이 런타임에 존재하는 실제 객체라고 해도,
key of 키워드는 일반적인 객체와 다르게 동작한다.
key of typeof 를 사용하면 모든 enum의 키를 문자열로 나타내는 타입을 가져온다.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* 이것은 아래와 동일합니다. :
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log("Log level key is:", key);
console.log("Log level value is:", num);
console.log("Log level message is:", message);
}
}
printImportant("ERROR", "This is a message");
역 매핑 (Reverse mappings)
숫자 enum 멤버는 멤버의 프로퍼티 이름을 가진 개체를 생성하는 것 외에도
enum 값에서 enum 이름으로 역 매핑이 가능하다.
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"
이렇게 생성된 코드에서 enum은 정방향(name -> value) 매핑과
역방향(value -> name) 매핑 두 정보를 모두 저장하는 객체로 컴파일된다.
다른 enum의 멤버를 참조하는 경우는 항상 프로퍼티 접근으로 노출되며,
인라인 되지 않는다.
문자열 enum은 역 매핑을 생성하지 않는다.
const enum
const enum을 사용하면
enum 값에 접근할 때 추가로 생성된 코드 및 추가적인 간접 참조에 대한 비용을 피할 수 있다.
const enum은 const 지정자를 enum에 붙여 정의한다.
const enum Enum {
A = 1,
B = A * 2,
}
const enum은 상수 enum 표현식만 사용될 수 있으며,
일반적인 enum과 달리 컴파일 과정에서 완전히 제거된다.
const enum은 사용하는 공간에 인라인된다.
이러한 동작은 const enum이 "계산된 멤버"를 갖지 않기 때문에 가능하다.
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
위 코드는 아래와 같이 컴파일 된다.
"use strict";
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */,
];
Objects VS Enums
as const를 사용하는 것만으로도 충분한다면
enum을 굳이 사용하지 않는 것이 좋다.
const status = {
SUCCESS: 'success',
ERROR: 'error',
LOADING: 'loading',
} as const;
type Status = (typeof status)[keyof typeof status];
// => 'success' | 'error' | 'loading'
as const는 TypeScript에서 값을 리터럴 타입으로 고정(freeze) 시키는 문법이다.
타입 추론 시 “가능한 가장 좁은 타입”으로 만들어 준다.
예를 들어 as const가 없을 때
const colors = ['red', 'blue', 'green'];
colors 배열 요소의 타입은 string[] 이 된다.
as const 를 사용하면
const colors = ['red', 'blue', 'green'] as const;
아래와 같이 타입을 추론한다.
readonly ["red", "blue", "green"]
즉, 배열 요소 각각을 리터럴 타입으로 고정하고,
배열도 readonly로 바뀐다.
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
EDirection.Up;
// (enum member) EDirection.Up = 0
ODirection.Up;
// (property) Up: 0
// Using the enum as a parameter
function walk(dir: EDirection) {}
// It requires an extra line to pull out the keys
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right);
JavaScript에는 enum 문법이 존재하지 않기 때문에
컴파일된 enum 객체가 남아, 런타임 오버헤드가 발생한다.
as const 는 JavaScript 객체이므로 추가 코드가 없어, 더 직관적인 방법이라고 할 수 있다.
참고 자료
'Node.js > TypeScript' 카테고리의 다른 글
[TypeScript] Item[] 타입과 빈배열 (0) | 2023.05.19 |
---|---|
[TypeScript] 비동기 - promise 안에 promise? Promise.all()로 처리 (0) | 2023.05.19 |