[Typescript] 열거형(Enum) & Literal Type
앞서 타입을 공부하면서, 값들을 나열한 열거형(Enum)에 대해서 공부했다.
이것이 적용될 방법 중 하나가 바로 이 Literal Type인 것이다. 얼핏 보았을때, 자료형이 아닌 특정값을 직접적으로 타입으로서 설정하는 방법인 것 같다.
💙 열거형(Enums)
열거(Enum)는 비원시 타입이자, 멤버라 불리는 값의 집합을 이루는 자료형이다. enum 키워드로 선언하며, {} 안에 자료형을 나열한다.
enum Fruits {
Apple,
Banana,
Kiwi,
}
열거형을 사용하는 대표적인 목적은, 특정 프로퍼티를 자료형보다는 값(혹은 값의 집합)으로 제한하고자 하는데 있다.
- Enum 적용
다음 예제에서, enum(Subject)로 설정한 subject 프로퍼티를 string('Typescript')로 설정하니 위처럼 오류가 발생했다.
이 프로퍼티를, 아래와 같이 특정 enum 요소를 지정하면 정상적으로 컴파일된다. (객체문법과 유사)
// app.ts 수정
function getStudentInfoEnum(studentId: number): StudentInfoEnum {
return {
studentId: 10,
studentName: 'Taeng',
age: 29,
subject: Subject.Typescript, // enum 요소지정
graduated: false,
}
}
- Enum 컴파일링
// app.js
var Subject;
(function (Subject) {
Subject[Subject["Java"] = 0] = "Java";
Subject[Subject["Javascript"] = 1] = "Javascript";
Subject[Subject["Typescript"] = 2] = "Typescript";
})(Subject || (Subject = {}));
function getStudentInfoEnum(studentId) {
return {
studentId: 10,
studentName: 'Taeng',
age: 29,
subject: Subject.Typescript,
graduated: false
};
}
다음은, TS파일을 컴파일한 JS파일이다. 보시다시피, Enum은 객체로 번역되어 추가되었다는 것을 알 수 있다. (런타임에 존재)
여기서 주목해야 할 점은, Subject[Subject[key] = number index] = "value" 형태로 각 요소를 타겟팅하고 있다.
💙 열거형(Enums) 종류
1. 숫자 열거형(Numeric Enum)
위 예제에서 알 수 있듯이, Enum은 다음과 같은 구조를 가진다. 이를, 숫자 열거형(Numeric Enum) 이라고 한다.
enum Subject {
Java = 1,
Javascript = 2,
Typescript = 3,
}
숫자 열거형은 각 멤버에 숫자값을 지정할수도 있으며, 지정된 값 외에는 자동을 +1씩 증가한다.
2. 문자형 열거형(String Enum)
만약 숫자 열거형을 원치 않는다면, Enum 각 멤버를 문자열로 초기화한다. 이것을, 문자형 열거형(String Enum) 이라 한다.
enum Subject {
Java = "java",
Javascript = "javascript",
Typescript = "typescript",
}
// compile -> app.js
var Subject;
(function (Subject) {
Subject["Java"] = "java";
Subject["Javascript"] = "javascript";
Subject["Typescript"] = "typescript";
})(Subject || (Subject = {}));
어떻게 보면, 일반 객체(Object)와 매우 유사한 형태이다. (필드를 ':' 이 아닌, '=' 로 연결한다는 차이?)
문자열 열거형은 숫자처럼 자동증가 기능은 없지만, 직렬화(멤버 정보를 직관적으로 표현하고 다이렉트 연결) 하여 읽기에 용이하다.
3. 이중 열거형(Heterogeneous Enum)
기술적으로, Enum을 숫자와 문자를 섞어 사용할 수 있지만 공식 가이드북 상에서도 굳이 권장하지 않는다. (런타임 이점이 없다면)
4. 유니언 열거형(Union Enum)
Literal Enum member(리터럴 열거형 멤버)는 초기화 값이 없는 상수멤버이거나, 숫자 혹은 문자열 리터럴로 초기화된 멤버를 의미한다.
열거형의 모든 멤버들이 리터럴 열거형 멤버에 해당할 경우, 2가지 특별한 동작을 보인다.
1) 아래처럼 각 멤버는 자료형으로써 취급될 수 있다. ShapeKind enum의 멤버인 Circle, Square 각각이 자료형으로서 인터페이스에 쓰임
enum ShapeKind {
Circle, // 0
Square, // 1
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square, // Error! Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'.
radius: 100,
}
2) 열거형 그 자체로 유니온 역할을 수행한다. 쉽게 말하면, 각 멤버들을 인식하고 있다고 할 수 있다.
그렇기에 아래처럼, Foo가 아닌 경우 무조건 Bar가 되기 때문에 이중 비교는 의미가 없는 것이다. (Foo | Bar)
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// 에러! E 타입은 Foo, Bar 둘 중 하나이기 때문에 이 조건은 항상 true를 반환합니다.
}
}
* 우리가 파이프(|)로 타입을 연결한 것이 유니언 타입이다. 고급 타입에 속하므로, 핸드북 '유니언과 교차타입'을 참고바란다.
(typescript-kr.github.io/pages/unions-and-intersections.html)
- Enum 관련용어
5. 계산된 멤버(Computed Members) vs 상수 멤버(Constant Members)
각 Enum의 멤버는 상수 혹은 계산된 값이다. 아래 3가지 경우는 상수, 이외의 경우에는 계산된 값을 간주한다.
1) Enum의 첫 번째 데이터이며, 초기화 값이 없는 경우. 0으로 값이 할당된다.
enum E { X }
2) 멤버 초기화 값이 없지만, 숫자 상수로 초기화된 열거형 멤버 뒤에 따라나오는 경우. +1 증가한 값이 할당된다.
enum E1 { X, Y, Z }
enum E2 {
A = 1, B, C
}
3) Enum 멤버가 상수 열거형 표현식으로 정의된 경우
- 리터럴 열거형 표현식 (기본적으로 문자 리터럴 또는 숫자 리터럴)
- 이전에 정의된 다른 상수 열거형 멤버에 대한 참조 (다른 열거형에서 시작될 수 있음)
- 괄호로 묶인 상수 열거형 표현식
- 상수 열거형 표현식에 단항 연산자 +, -, ~ 를 사용한 경우
- 상수 열거형 표현식을 이중 연산자 +, -, *, /, %, <<, >>, >>>, &, |, ^ 의 피연산자로 사용할 경우
enum FileAccess {
// 상수 멤버
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 계산된 멤버
G = "123".length
}
6. const 열거형(const Enum)
열거형 값에 접근할 때, 추가적인 간접참조를 피하기 위해 사용되는 문법이다. 상수 열거 표현식만 사용 가능하다.
const enum Enum {
A = 1,
B = A * 2
}
7. Ambient 열거형(Ambient Enum)
이미 존재하는 열거형 타입의 형태를 묘사하기 위해 사용된다. declare 키워드를 사용한다.
아래처럼 초기화되지 않고 포위된 B는 계산된 멤버(Computed Member)로서 취급된다.
declare enum Enum {
A = 1,
B,
C = 2
}
💙 리터럴 타입(Literal Type)
앞서, 값들의 집합으로 프로퍼티를 제한하기 위해 열거(Enum)를 적용했다.
이를, 적은 값으로 직관적으로 제한하기 위해 적용하는 것이 리터럴 타입(Literal Type)이다. 집합타입의 하위이나, 구체적인 타입이다.
// enum
enum Subject {
Java,
Javascript,
Typescript
}
interface StudentInfoEnum {
readonly studentId: number,
studentName: string,
age: number,
subject: Subject,
graduated: boolean,
}
// literal type
interface StudentInfoLiteral {
readonly studentId: number,
studentName: string,
age: number,
subject: 'java' | 'javascript' | 'typescript',
graduated: boolean,
}
이처럼, 프로퍼티에 직접적으로 제한할 값들을 작성한다. 여러 값으로 제한하고자 할때는, 값들을 '|'(파이프)로 연결하면 된다.
Typescript에는 문자열과 숫자, 두 가지 Literal Type이 있는데 이에 대한 정확한 값을 지정하는 문법이다.
당연히, Literal Type으로 제한한 값들 외의 값을 적용하려고 하면 에러가 발생한다.
- 문자형 리터럴 타입 (String Literal Type)
값으로 할당할 문자열들을 유니언으로 나열한다. Enum과 비슷한 형태를 가진다.
type Easing = "ease-in" | "ease-out" | "ease-in-out"; // 문자열 리터럴 타입
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
} else if (easing === "ease-out") {
} else if (easing === "ease-in-out") {
} else {
// 하지만 누군가가 타입을 무시하게 된다면
// 이곳에 도달하게 될 수 있습니다.
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error! Easing 리터럴 타입에 포함되지 않음
- 숫자형 리터럴 타입 (Numeric Literal Type)
문자와 마찬가지로, 숫자도 리터럴 타입으로 설정할 수 있다.
function rollDice(): 1 | 2 | 3 | 4 | 5 | 6 {
return (Math.floor(Math.random() * 6) + 1) as 1 | 2 | 3 | 4 | 5 | 6;
}
const result = rollDice();
열거형의 구체적인 개념과 다양한 문법들에 대해 공부할 수 있었다. 또한, 리터럴 타입이라는 직관적이고 유용한 방법도 알게 되었다.
이 둘을 작성하면서(특히 리터럴에서), 유니언(과 교차타입)에 대한 거론이 나올 수 밖에 없었다.
이 둘을 포함한 고급 타입에 대해서도 조만간 포스팅을 해야겠다.
[출처]
- Typescript 핸드북 : typescript-kr.github.io/pages/basic-types.html#%ED%83%80%EC%9E%85-%EB%8B%A8%EC%96%B8-type-assertions
- Typescript 가이드북 : yamoo9.gitbook.io/typescript/types/type-assertions
- kjwsx23 님의 블로그 : kjwsx23.tistory.com/450?category=746259.
- 땅콩코딩 님의 Youtube강의 : www.youtube.com/watch?v=-TlpYcmHvb8&list=PLJf6aFJoJtbUXW6T4lPUk7C66yEneX7MN&index=6