Typescript에서 가독성을 높이기 위한 일환으로 서로 연관된 상수들을 하나의 namespace에 묶어 관리할 때, 다음과 같이 enum
키워드를 사용해 Enum type을 선언하거나
enum COLOR {
red,
blue,
green
}
객체 리터럴에 as const
라는 type assertion을 사용합니다.
const COLOR = {
red: 0,
blue: 1,
green: 2
} as const;
두 경우 모두 IDE의 자동완성 기능을 활용할 수 있고, 객체의 프로퍼티들이 모두 readonly
로 다뤄지기 때문에 변경의 우려도 없습니다.
위 예제만 본다면 어떤 것을 사용해도 무방하지만, 두 문법은 명확한 차이점이 있습니다.
사실 enum
과 as const
는 탄생한 목적 자체가 다릅니다. enum
은 다른 언어의 Enumeration 문법처럼 서로 연관된 상수들을 하나의 namespace로 묶어 추상화시키기 위해 도입된 것입니다. 이를 통해 코드만 보더라도 의도를 명확히 알 수 있어 가독성이 높일 수 있습니다.
그에 반해 as const
는 type assertion의 한 종류로써 리터럴 타입의 추론 범위를 줄이고 값의 재할당을 막기 위한 목적으로 만들어졌습니다.
... When we construct new literal expressions with
const
assertions, we can signal to the language that
- no literal types in that expression should be widened (e.g. no going from
"hello"
tostring
)- object literals get
readonly
properties- array literals become
readonly
tupleshttps://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions
const greeting1 = "Hello, World!"; // "Hello, World!"로 추론
let greeting2 = "Hello, World!"; // string으로 추론
Typescript는 const
로 선언할 경우, 자동적으로 리터럴 자체로 타입을 추론합니다. 하지만 let
으로 선언할 경우, 리터털이 속한 타입으로 타입을 추론합니다. 즉, greeting2
라는 변수는 Hello, World!
라는 리터럴 타입으로 한정되지 않고, string
이라는 상위 타입으로 범위가 확장되는 것입니다.
만약 let으로 선언된 변수의 타입을 리터럴로 한정하고 싶다면,
let greeting2 = "Hello, World!" as const;
와 같이 as const
로 type assertion을 해주면 됩니다. const
로 선언된 것과 똑같은 효과를 일으키기 때문에 Hello, World
로 타입이 추론되고, 변수에 새로운 값을 할당할 수 없습니다. (물론, 위와 같이 쓸바에야 const
로 선언하는 것이 더 맞겠죠...)
하지만 원시 타입이 아닌 object
나 array
타입의 경우라면 얘기가 달라집니다. object
나 array
는 참조 타입이기 때문에 const
로 선언하더라도, 내부 프로퍼티의 추론 범위가 한정되지도 않고, 값을 변경할 수도 있습니다.
const language = {
korean: "ko", // string으로 추론
english: "en", // string으로 추론
};
language.korean = "kkk"; // kkk로 korean프로퍼티가 변경
language.english = "eee"; // eee로 english프로퍼티가 변경
as const
을 사용한다면,
const language = {
korean: "ko", // "ko"로 추론, readonly 프로퍼티로 변경
english: "en", // "en"로 추론, readonly 프로퍼티로 변경
} as const;
// Error: Cannot assign to 'korean' because it is a read-only property
language.korean = "kkk";
// Error: Cannot assign to 'english' because it is a read-only property.
language.english = "eee";
객체의 모든 프로퍼티들이 readonly
로 변경되고, 각 프로퍼티의 타입이 할당된 리터럴 값으로 추론됩니다.
심지어 다음과 같이 중첩된 객체나 배열의 프로퍼티들도readonly
로 변경되고 리터럴 값으로 추론됩니다.
const obj = {
a: 10,
b: [20, 30],
c: {
d: {
e: {
greeting: "Hello",
},
},
},
} as const;
// 다음과 같이 추론됨.
const obj: {
readonly a: 10;
readonly b: readonly [20, 30];
readonly c: {
readonly d: {
readonly e: {
readonly greeting: "Hello";
};
};
};
};
이렇듯, as const
를 사용하면 원시 타입이든 참조 타입이든 값의 재할당을 막아버리기 때문에 의도치 않은 변경으로 인한 오류를 없앨 수 있습니다. 또한, 리터럴 타입의 추론 범위가 리터럴 값 자체로 한정되면서 좀 더 안전하게 코드를 작성할 수 있습니다.
객체 리터럴에 as const
를 사용하면 enum
과 매우 유사하게 사용할 수 있지만, 상수들을 하나로 묶어 추상화시킬 목적이라면, 애초에 이를 목적으로 도입된 enum
을 사용하는 것이 더 나아보입니다.
사실 enum
은 as const
가 아닌 const enum
와 비교해야 합니다. 둘 다 상수의 추상화라는 목적은 같지만, 트랜스파일된 결과는 아예 다르기 때문에 상황에 따라 어떤 것을 사용할 지 판단할 필요가 있습니다.
// transpile 이전
enum COLOR {
red,
blue,
green
}
// transpile 이후
var COLOR;
(function (COLOR) {
COLOR[COLOR["red"] = 0] = "red";
COLOR[COLOR["blue"] = 1] = "blue";
COLOR[COLOR["green"] = 2] = "green";
})(COLOR || (COLOR = {}));
트랜스파일 이후 코드를 보면 한 눈에 파악하기 힘들어보이지만, 사실은
var COLOR = {
red: 0,
blue: 1,
green: 2,
'0': 'red',
'1': 'blue',
'2': 'green'
}
와 같은 형태의 객체로 바뀌는 것입니다. 즉, 각 프로퍼티에 특정 값들이 매핑됨과 동시에 역방향으로도 매핑이 됩니다. 이를 reverse mapping라 표현합니다.
// transpile 이전
const enum COLOR {
red,
blue,
green,
}
console.log(COLOR.red);
console.log(COLOR.blue);
console.log(COLOR.green);
// transplie 이후
console.log(0 /* red */);
console.log(1 /* blue */);
console.log(2 /* green */);
트랜스파일 이후 코드를 보면, 어떠한 객체 리터럴도 존재하지 않습니다. Typescript는 const enum
의 멤버에 접근하는 코드를 각 멤버의 값으로 치환하지만, enum
과 달리 선언된 const enum
객체를 최종 결과물에 포함시키지 않습니다.
enum
의 reverse mapping은 유용한 경우도 있지만, 대부분의 경우 쓸모가 없습니다. 만약 Color.red
가 아니고 Color[0]
라 작성할 경우, 의도한 색이 무엇인지 코드만 보고 파악할 수 없기 때문에 절대로 Color[0]
형태로 작성하지 않습니다.
이 말은 즉슨, 트랜스파일의 결과로 생성된 코드에 불필요한 코드가 추가된다는 뜻입니다. 그에 반해 const enum
는 객체 리터럴 조차 결과에 남지 않기 때문에 훨씬 더 적은 코드를 만듭니다. 실제 배포되는 코드의 크기는 티끌만큼이라도 줄이는 것이 도움되기 때문에 reverse mapping이 필요한 경우가 아니라면 const enum
을 사용하는 것이 좋습니다.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
function logImportant(level: LogLevel, message: string): void {
if (level > LogLevel.WARN) return;
const logLevel = LogLevel[level];
console.log(`[${logLevel}] ${message}`);
}
// logImportant(0, "This is a error level message.");
logImportant(LogLevel.ERROR, "This is a error level message.");
// logImportant(1, "This is a warn level message.");
logImportant(LogLevel.WARN, "This is a warn level message.");
// logImportant(2, "This is a info level message.");
logImportant(LogLevel.INFO, "This is a info level message.");
// logImportant(3, "This is a debug level message.");
logImportant(LogLevel.DEBUG, "This is a debug level message.");
// 출력
// [ERROR] This is a error level message.
// [WARN] This is a warn level message.
https://thoughtbot.com/blog/the-trouble-with-typescript-enums
이해하기 쉬운 설명 감사합니다 :)