타입스크립트 Enum 정복: 실전 가이드 & 완벽 대안

타입스크립트 Enum 정복: 실전 가이드 & 완벽 대안

이번 글에서는 타입스크립트 열거형(Enum)에 대해 자세히 살펴보려고 합니다.

  • 열거형은 어떻게 작동할까요?
  • 어떤 경우에 사용하면 좋을까요?
  • 사용하고 싶지 않을 때는 어떤 대안이 있을까요?

마지막에는 어떤 상황에 무엇을 사용하는 것이 좋은지 추천해드리면서 글을 마무리하려고 합니다.

 

 

표기법

 

소스 코드에서 유추된 타입을 표시하기 위해 npm 패키지 ts-expect를 사용했는데요.

 

예를 들면 다음과 같습니다.

// Types of values
// @ts-expect-error: Argument of type 'number' is not assignable to parameter of type 'string'.
expectType<string>(123);
// @ts-expect-error: Argument of type 'string' is not assignable to parameter of type 'number'.
expectType<number>('abc');

// Equality of types
type Pair<T> = [T, T];
// @ts-expect-error: Argument of type 'false' is not assignable to parameter of type 'true'.
expectType<TypeEqual<Pair<string>, [string,string]>>(false);

 

 

타입스크립트 열거형 기본

 

다양한 프로그래밍 언어에는 여러 종류의 열거형이 존재합니다.

 

타입스크립트에서 열거형은 다음 두 가지를 정의합니다.

  • 멤버 키를 멤버 값에 매핑하는 객체
  • 멤버 값만 포함하는 타입

이번 블로그 글에서는 const 열거형은 다루지 않는다는 점을 참고해 주세요.

 

이제 열거형의 다양한 측면을 자세히 살펴보겠습니다.

 

 

열거형은 객체를 정의합니다

 

우선, 열거형은 멤버 키를 멤버 값에 매핑하는 객체입니다.

 

이런 면에서 객체 리터럴과 매우 유사하게 작동하는데요.

enum Color {
  Red = 0,
  Green = 'GREEN',
}
console.log(Color.Red === 0); // true
console.log(Color.Green === 'GREEN'); // true
console.log(Color['Green'] === 'GREEN'); // true

 

한 가지 제약은 멤버 값으로 숫자와 문자열만 허용된다는 것입니다.

 

 

열거형은 타입을 정의합니다

 

또 한편으로, 열거형은 멤버 값만 포함하는 타입이기도 합니다.

let color: Color;
color = Color.Red;
color = Color.Green;
// @ts-expect-error: Type 'true' is not assignable to type 'Color'.
color = true;

 

문자열 멤버와 숫자 멤버 사이에는 중요한 차이점이 하나 있는데요. color에 일반 문자열을 할당할 수 없다는 것입니다.

// @ts-expect-error: Type '"GREEN"' is not assignable to
// type 'Color'.
color = 'GREEN';

 

하지만 일반 숫자는 color에 할당할 수 있습니다. 단, 그 숫자가 멤버의 값인 경우에만 가능합니다.

color = 0;
// @ts-expect-error: Type '123' is not assignable to type 'Color'.
color = 123;

 

 

열거형에 대한 망라성 검사를 할 수 있습니다

 

다음 열거형을 살펴보겠습니다.

enum Color {
  Red = 0,
  Green = 'GREEN',
}

 

Color 타입의 변수가 가질 수 있는 값을 처리할 때, 그 중 하나라도 누락되면 타입스크립트가 경고를 표시할 수 있습니다.

 

다시 말해, 모든 경우를 "망라적으로" 처리하지 않은 경우입니다.

 

이를 망라성 검사(exhaustiveness check)라고 합니다.

 

작동 방식을 알아보기 위해 다음 코드로 시작해 보겠습니다.

// @ts-expect-error: Not all code paths return a value.
function colorToString(color: Color) {
  expectType<Color>(color); // (A)
  if (color === Color.Red) {
    return 'red';
  }
  expectType<Color.Green>(color); // (B)
  if (color === Color.Green) {
    return 'green';
  }
  expectType<never>(color); // (C)
}

 

A 라인에서 color는 여전히 모든 값을 가질 수 있습니다.

 

B 라인에서는 Color.Red를 제외했으므로 colorColor.Green 값만 가질 수 있습니다.

 

C 라인에서 color는 어떤 값도 가질 수 없으므로 타입이 never가 됩니다.

 

C 라인에서 colornever가 아니라면 멤버를 잊어버린 것입니다.

 

다음과 같이 컴파일 타임에 타입스크립트가 오류를 보고하도록 할 수 있습니다.

function colorToString(color: Color) {
  if (color === Color.Red) {
    return 'red';
  }
  if (color === Color.Green) {
    return 'green';
  }
  throw new UnsupportedValueError(color);
}

 

어떻게 작동하는 걸까요?

 

UnsupportedValueError에 전달하는 값은 never 타입이어야 합니다.

class UnsupportedValueError extends Error {
  constructor(value: never, message = `Unsupported value: ${value}`) {
    super(message)
  }
}

 

두 번째 경우를 잊어버리면 다음과 같은 일이 발생합니다.

function colorToString(color: Color) {
  if (color === Color.Red) {
    return 'red';
  }
  // @ts-expect-error: Argument of type 'Color.Green'
  // is not assignable to parameter of type 'never'.
  throw new UnsupportedValueError(color);
}

 

망라성 검사는 case 문에서도 잘 작동합니다.

function colorToString(color: Color) {
  switch (color) {
    case Color.Red:
      return 'red';
    case Color.Green:
      return 'green';
    default:
      throw new UnsupportedValueError(color);
  }
}

 

망라성을 검사하는 또 다른 방법은 함수의 반환 타입을 지정하는 것입니다.

// @ts-expect-error: Function lacks ending return statement and
// return type does not include 'undefined'.
function colorToString(color: Color): string {
  switch (color) {
    case Color.Red:
      return 'red';
  }
}

 

제 코드에서는 보통 이렇게 하지만, 런타임에도 작동하는 검사를 원하기 때문에 추가로 UnsupportedValueError를 발생시킵니다.

 

 

멤버 열거

 

가끔 유용한 작업 중 하나는 열거형의 멤버를 열거하는 것입니다.

 

타입스크립트 열거형으로 할 수 있을까요? 이전 열거형을 사용해 보겠습니다.

enum Color {
  Red = 0,
  Green = 'GREEN',
}

 

자바스크립트로 컴파일된 Color는 다음과 같습니다.

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red"; // (A)
    Color["Green"] = "GREEN"; // (B)
})(Color || (Color = {}));

 

이 함수는 즉시 호출되어 객체 Color에 프로퍼티를 추가합니다.

 

B 라인의 문자열 멤버 Green에 대한 코드는 간단합니다.

 

키를 값에 매핑합니다.

 

A 라인의 숫자 멤버 Red에 대한 코드는 Red에 대해 하나가 아닌 두 개의 프로퍼티를 추가합니다.

 

키에서 값으로의 매핑과 값에서 키로의 매핑입니다.

Color["Red"] = 0;
Color[0] = "Red";

 

두 번째 라인의 0은 문자열로 강제 변환됩니다(프로퍼티 키는 문자열 또는 심볼만 가능).

 

따라서 실제로 이런 식으로 숫자를 조회할 수는 없습니다.

 

먼저 문자열로 변환해야 합니다.

 

따라서 숫자 멤버는 이 열거형의 키나 값을 열거하는 것을 방해합니다.

console.log(Object.keys(Color)); // ['0', 'Red', 'Green']
console.log(Object.values(Color)); // ['Red', 0, 'GREEN']

 

문자열 멤버로만 전환하면 열거가 작동합니다.

enum Color {
  Red = 'RED',
  Green = 'GREEN',
}

console.log(Object.keys(Color)); // ['Red', 'Green']
console.log(Object.values(Color)); // ['RED', 'GREEN']

 

 

명시적으로 지정된 값이 없는 열거형

 

명시적으로 멤버 값을 지정하지 않고 열거형을 생성할 수도 있습니다.

 

그러면 타입스크립트가 값을 지정하고 숫자를 사용합니다.

enum Color {
  Red, // 암시적으로 = 0
  Green, // 암시적으로 = 1
}
console.log(Color.Red === 0); // true
console.log(Color.Green === 1); // true

 

 

열거형의 사용 사례

 

열거형과 열거형 관련 패턴을 이해하는 것은 금방 혼란스러워질 수 있습니다.

 

왜냐하면 열거형이 무엇인지가 프로그래밍 언어마다 매우 다르기 때문입니다.

 

예를 들면 다음과 같습니다.

  • 자바의 열거형은 고정된 인스턴스 집합을 가진 클래스입니다.
  • 러스트의 열거형은 함수형 프로그래밍 언어의 대수적 데이터 타입과 더 유사합니다. 타입스크립트의 판별된 유니온과 느슨하게 관련되어 있습니다.

따라서 열거형이라는 용어를 좁게 정의하는 것이 도움이 됩니다.

  • 고정된 값 집합.
  • 객체의 키를 통해 접근할 수 있습니다.

다음 섹션에서는 이러한 종류의 열거형에 대한 다음 사용 사례를 살펴보겠습니다.

  • 원시 값을 가진 상수의 네임스페이스
  • 고유한 값을 가진 사용자 정의 타입
  • 객체 값을 가진 상수의 네임스페이스

타입스크립트 열거형이 이러한 사용 사례에 얼마나 적합한지 살펴보겠습니다.

 

그리고 대신 사용할 수 있는 열거형과 유사한 패턴을 살펴보겠습니다.

 

각 옵션에 대해 다음 사항도 검토합니다.

  • 망라성 검사를 수행할 수 있는지 여부
  • 멤버를 열거할 수 있는지 여부

 

 

사용 사례: 원시 값을 가진 상수의 네임스페이스

 

열거형(또는 열거형과 유사한 객체)이 사용되는 한 가지 방법은 단순히 상수의 네임스페이스로 사용하는 것입니다.

 

예를 들어, Node.js 함수 fs.access()에는 mode라는 매개변수가 있는데, 이 매개변수의 값은 다음 열거형과 유사한 객체를 통해 제공됩니다.

enum constants {
  F_OK = 0,
  R_OK = 4,
  W_OK = 2,
  X_OK = 1,
  // ...
}

 

첫 번째 값을 제외하고는 비트 OR을 통해 결합되는 비트입니다.

const readOrWrite = constants.R_OK | constants.W_OK;

 

 

원시 값을 가진 상수의 네임스페이스로서의 열거형

 

이 사용 사례와 관련된 열거형 기능은 무엇일까요?

  • 열거형의 주요 제한 사항 중 하나는 값이 숫자 또는 문자열만 될 수 있다는 것입니다.
  • 열거형은 타입으로서 중요하지 않습니다. 매개변수 mode의 타입은 constants와 같은 것이 아니라 number입니다. 왜냐하면 constants의 값은 mode의 모든 가능한 값의 망라적인 목록이 아니기 때문입니다.
  • 같은 이유로, 이 경우에는 망라성 검사가 관련이 없습니다.
  • 멤버를 열거하는 것도 바람직하지 않습니다. 그리고 숫자 값을 가진 열거형으로는 할 수 없는 일입니다.

 

열거형의 대안: 객체 리터럴

 

이 사용 사례의 경우 객체 리터럴이 매우 좋은 대안입니다.

const constants = {
  __proto__: null,
  F_OK: 0,
  R_OK: 4,
  W_OK: 2,
  X_OK: 1,
};

 

가짜 프로퍼티 키 __proto__를 사용하여 constants의 프로토타입을 null로 설정합니다. 이렇게 하면 상속된 프로퍼티를 처리할 필요가 없으므로 좋은 관행입니다.

  • 주요 이점은 .toString과 같은 Object.prototype에서 상속된 프로퍼티에 대해 걱정하지 않고 in 연산자를 사용하여 constants에 주어진 키가 있는지 확인할 수 있다는 것입니다.
  • 하지만 Object.keys()Object.values()는 어차피 상속된 프로퍼티를 무시하므로 여기서는 얻을 것이 없습니다.

참고로 __proto__Object.prototype에도 게터와 세터로 존재합니다.

 

이 기능은 Object.getPrototypeOf()Object.setPrototypeOf()를 사용하는 것이 좋기 때문에 더 이상 사용되지 않습니다.

 

하지만 객체 리터럴에서 이 이름을 사용하는 것과는 다르며, 이는 더 이상 사용되지 않습니다.

 

자세한 내용은 "Exploring JavaScript"의 다음 섹션을 확인해 주세요.

 

 

사용 사례: 고유한 값을 가진 사용자 정의 타입

 

때로는 고정된 값 집합을 가진 사용자 정의 타입을 정의하고 싶을 때가 있습니다.

 

예를 들어, 불리언은 의도를 잘 표현하지 못하는 경우가 있습니다.

 

그럴 때는 열거형이 더 나은 역할을 할 수 있습니다.

enum Activation {
  Active = 'Active',
  Inactive = 'Inactive',
}

 

=를 통해 명시적으로 문자열 값을 지정하는 것이 좋습니다.

  • 타입 안전성이 향상되고 Activation이 예상되는 곳에 실수로 숫자를 제공할 수 없습니다.
  • Activation의 키와 값을 열거할 수 있습니다.

 

고유한 값을 가진 사용자 정의 타입으로서의 열거형

 

이 사용 사례와 관련된 열거형 기능은 무엇일까요?

  • Activation에서 정의한 타입을 사용할 것입니다.
  • 망라성 검사가 가능하고 유용합니다.
  • 키나 값을 열거하고 싶을 수도 있습니다.

 

열거형의 대안: 객체 리터럴

 

객체 리터럴을 사용하여 열거형의 값 부분을 정의해 보겠습니다(타입 부분은 다음에 다루겠습니다).

const Activation = {
  __proto__: null,
  Active: 'Active',
  Inactive: 'Inactive',
} as const; // (A)

// `as const`가 없으면 이 타입은 `string`이 됩니다.
console.log(Activation.Active); // 'Active'

type ActivationType = PropertyValues<typeof Activation>;
type TypeEqual<A, B> = A extends B ? (B extends A ? true : false) : false;
console.log((true as unknown) === (true as TypeEqual<ActivationType, 'Active' | 'Inactive'>)); // true

 

A 라인의 as const를 사용하면 도우미 타입 PropertyValues(아래에 정의됨)를 통해 Activation에서 ActivationType을 유도할 수 있습니다.

 

이 타입이 Activation이 아니라 ActivationType이라고 불리는 이유는 무엇일까요?

 

타입스크립트에서 값과 타입의 네임스페이스가 분리되어 있기 때문에 실제로 같은 이름을 사용할 수 있습니다.

 

하지만 Visual Studio Code를 사용하여 값과 타입의 이름을 바꿀 때 문제가 있었습니다.

Activation을 가져오면 값과 타입을 모두 가져오기 때문에 혼란스러워했습니다.

 

그래서 지금은 다른 이름을 사용하고 있습니다.

 

도우미 타입 PropertyValues는 다음과 같습니다.

type PropertyValues<Obj> = Obj[Exclude<keyof Obj, '__proto__'>];

 

타입 Obj[K]는 키가 K에 있는 모든 프로퍼티의 값을 포함합니다.

 

__proto__ 키는 keyof Obj에서 제외하는데, 타입스크립트가 이 키를 일반 프로퍼티로 취급하고 우리가 원하는 것이 아니기 때문입니다.

 

as const를 사용하지 않으면 유도된 타입이 어떻게 보이는지 살펴보겠습니다.

const Activation = {
  __proto__: null,
  Active: 'Active',
  Inactive: 'Inactive',
};

console.log(Activation.Active); // string
console.log(Activation.Inactive); // string

type ActivationType = PropertyValues<typeof Activation>;
console.log((true as unknown) === (true as TypeEqual<ActivationType, string>)); // true

 

 

망라성 검사

 

타입스크립트는 리터럴 타입의 유니온에 대한 망라성 검사를 지원합니다.

 

그리고 그것이 바로 ActivationType입니다.

 

따라서 열거형과 동일한 패턴을 사용할 수 있습니다.

function activationToString(activation: ActivationType): string {
  switch (activation) {
    case Activation.Active:
      return 'ACTIVE';
    case Activation.Inactive:
      return 'INACTIVE';
    default:
      throw new UnsupportedValueError(activation);
  }
}

 

 

멤버 열거

 

Object.keys()Object.values()를 사용하여 객체 Activation의 멤버를 열거할 수 있습니다.

for (const value of Object.values(Activation)) {
  console.log(value);
}

 

출력:

Active
Inactive

 

 

프로퍼티 값으로 심볼 사용

 

프로퍼티 값으로 문자열을 사용하는 한 가지 단점은 ActivationType이 임의의 문자열 사용을 배제하지 않는다는 것입니다.

 

심볼을 사용하면 타입 안전성을 더 높일 수 있습니다.

const Active = Symbol('Active');
const Inactive = Symbol('Inactive');

const Activation = {
  __proto__: null,
  Active,
  Inactive,
} as const;

console.log(Activation.Active); // Symbol(Active)

type ActivationType = PropertyValues<typeof Activation>;
console.log((true as unknown) === (true as TypeEqual<
    ActivationType, typeof Active | typeof Inactive
  >)); // true

 

이것은 지나치게 복잡해 보입니다.

 

심볼을 사용하기 전에 먼저 심볼에 대한 변수를 선언하는 중간 단계가 왜 필요한 것일까요?

 

객체 리터럴 내에서 심볼을 생성하지 않는 이유는 무엇일까요?

 

아쉽게도 심볼에 대한 as const의 현재 한계입니다.

 

고유한 것으로 인식되지 않습니다.

const Activation = {
  __proto__: null,
  Active: Symbol('Active'),
  Inactive: Symbol('Inactive'),
} as const;

// 아쉽게도 `Activation.Active`의 타입은 `typeof Active`가 아닙니다.
console.log(Activation.Active); // symbol

type ActivationType = PropertyValues<typeof Activation>;
console.log((true as unknown) === (true as TypeEqual<ActivationType, symbol>)); // true

 

 

열거형의 대안: 문자열 리터럴 타입의 유니온

 

문자열 리터럴 타입의 유니온은 고정된 멤버 집합을 가진 타입을 정의할 때 열거형의 흥미로운 대안입니다.

type Activation = 'Active' | 'Inactive';

 

이러한 타입은 열거형과 어떻게 비교될까요?

 

장점:

  • 빠르고 간단한 솔루션입니다.
  • 망라성 검사를 지원합니다.
  • Visual Studio Code에서 멤버 이름 바꾸기가 잘 작동합니다.

단점:

  • 타입 멤버가 고유하지 않습니다. 심볼을 사용하면 변경할 수 있지만, 문자열 리터럴 유니온 타입의 편리함 중 일부를 잃게 됩니다. 예를 들어, 값을 가져와야 합니다.
  • 멤버를 열거할 수 없습니다. 다음 섹션에서는 이를 변경하는 방법을 설명합니다.

 

Set을 통한 문자열 리터럴 유니온 구체화

 

구체화란 메타 레벨(타입스크립트 타입)에 존재하는 엔티티에 대해 객체 레벨(자바스크립트 값)에서 엔티티를 생성하는 것을 의미합니다.

 

Set을 사용하여 문자열 리터럴 유니온 타입을 구체화할 수 있습니다.

const activation = new Set([
  'Active',
  'Inactive',
] as const);
console.log(activation); // Set(2) { 'Active', 'Inactive' }

// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type '"Active" | "Inactive"'.
activation.has('abc');
  // .has(), .delete() 등의 인수에 대해 자동 완성이 작동합니다.

// Set을 문자열 리터럴 유니온으로 변환해 보겠습니다.
type Activation = SetElementType<typeof activation>;
console.log((true as unknown) === (true as TypeEqual<Activation, 'Active' | 'Inactive'>)); // true

type SetElementType<S extends Set<any>> =
  S extends Set<infer Elem> ? Elem : never;

 

 

사용 사례: 객체 값을 가진 상수의 네임스페이스

 

때로는 객체에 저장된 더 풍부한 데이터를 조회하기 위해 열거형과 유사한 구조가 유용할 때가 있습니다.

 

객체를 열거형 값으로 사용할 수 없으므로 다른 솔루션을 사용해야 합니다.

 

 

프로퍼티 값이 객체인 객체 리터럴

 

다음은 객체 리터럴을 객체에 대한 열거형으로 사용하는 예입니다.

// 이 타입은 선택 사항입니다. `TextStyle`의 프로퍼티 값을 제한하지만
// 다른 용도는 없습니다.
type TTextStyle = {
  key: string,
  html: string,
  latex: string,
};
const TextStyle = {
  Bold: {
    key: 'Bold',
    html: 'b',
    latex: 'textbf',
  },
  Italics: {
    key: 'Italics',
    html: 'i',
    latex: 'textit',
  },
} as const satisfies Record<string, TTextStyle>;

type TextStyleType = PropertyValues<typeof TextStyle>;
type PropertyValues<Obj> = Obj[Exclude<keyof Obj, '__proto__'>];

 

 

망라성 검사

TextStyle의 프로퍼티 값에 .key 프로퍼티가 있는 이유는 무엇일까요?

 

이 프로퍼티를 사용하면 프로퍼티 값이 판별된 유니온을 형성하기 때문에 망라성 검사를 수행할 수 있습니다.

function f(textStyle: TextStyleType): string {
  switch (textStyle.key) {
    case TextStyle.Bold.key:
      return 'BOLD';
    case TextStyle.Italics.key:
      return 'ITALICS';
    default:
      throw new UnsupportedValueError(textStyle); // `.key`가 없습니다!
  }
}

 

비교를 위해 TextStyle이 열거형인 경우 f()는 다음과 같습니다.

enum TextStyle2 { Bold, Italics }
function f2(textStyle: TextStyle2): string {
  switch (textStyle) {
    case TextStyle2.Bold:
      return 'BOLD';
    case TextStyle2.Italics:
      return 'ITALICS';
    default:
      throw new UnsupportedValueError(textStyle);
  }
}

 

 

열거형 클래스

클래스를 열거형으로 사용할 수도 있습니다.

 

이는 자바에서 차용한 패턴입니다.

class TextStyle {
  static Bold = new TextStyle(/*...*/);
  static Italics = new TextStyle(/*...*/);
}
type TextStyleKeys = EnumKeys<typeof TextStyle>;
console.log((true as unknown) === (true as TypeEqual<TextStyleKeys, 'Bold' | 'Italics'>)); // true

type EnumKeys<T> = Exclude<keyof T, 'prototype'>;

 

이 패턴의 장점 중 하나는 메서드를 사용하여 열거형 값에 동작을 추가할 수 있다는 것입니다.

 

단점은 망라성 검사를 할 수 있는 간단한 방법이 없다는 것입니다.

 

Object.keys()Object.values().prototype과 같은 TextStyle의 열거 불가능한 프로퍼티를 무시합니다.

 

따라서 키와 값을 열거하는 데 사용할 수 있습니다. 예를 들면 다음과 같습니다.

console.log(
  // TextStyle.prototype은 열거 불가능합니다.
  Object.keys(TextStyle)
); // ['Bold', 'Italics']

 

 

열거형으로 매핑 및 열거형에서 매핑

 

때로는 열거형 값을 다른 값으로 변환하거나 그 반대로 변환하고 싶을 때가 있습니다.

 

예를 들어, JSON으로 직렬화하거나 JSON에서 역직렬화할 때입니다.

 

Map을 통해 이렇게 하면 타입스크립트를 사용하여 열거형 값을 잊어버린 경우 경고를 받을 수 있습니다.

 

어떻게 작동하는지 알아보기 위해 다음 열거형 패턴 타입을 사용하겠습니다.

const Pending = Symbol('Pending');
const Ongoing = Symbol('Ongoing');
const Finished = Symbol('Finished');
const TaskStatus = {
  __proto__: null,
  Pending,
  Ongoing,
  Finished,
} as const;
type TaskStatusType = PropertyValues<typeof TaskStatus>;
type PropertyValues<Obj> = Obj[Exclude<keyof Obj, '__proto__'>];

 

Map은 다음과 같습니다.

const taskPairs = [
  [TaskStatus.Pending, 'not yet'],
  [TaskStatus.Ongoing, 'working on it'],
  [TaskStatus.Finished, 'finished'],
] as const;

type Key = (typeof taskPairs)[number][0];
const taskMap = new Map<Key, string>(taskPairs);

 

taskPairs의 값을 new Map()의 인수로 직접 사용하지 않고 타입 매개변수를 생략하지 않은 이유가 궁금하다면, 타입스크립트는 키가 심볼인 경우 타입 매개변수를 추론할 수 없는 것 같고 컴파일 타임 오류를 보고하기 때문입니다.

 

문자열을 사용하면 코드가 더 간단해집니다.

const taskPairs = [
  ['Pending', 'not yet'],
  ['Ongoing', 'working on it'],
  ['Finished', 'finished'],
] as const;
const taskMap = new Map(taskPairs); // 타입 매개변수가 없습니다!

 

마지막 단계는 TaskStatus의 값 중 하나만 잊어버렸는지 확인하는 것입니다.

console.log((true as unknown) === (true as TypeEqual<MapKey<typeof taskMap>, TaskStatusType>)); // true
type MapKey<M extends Map<any, any>> =
  M extends Map<infer K, any> ? K : never;

 

 

권장 사항

 

언제 열거형을 사용하고 언제 대안 패턴을 사용해야 할까요?

 

타입스크립트 열거형은 자바스크립트가 아닙니다.

 

열거형은 해당하는 자바스크립트 기능이 없는 몇 안 되는 타입스크립트 언어 구조(타입 구조와 비교) 중 하나입니다. 이는 두 가지 방식으로 중요할 수 있습니다.

  • 트랜스파일된 코드가 약간 이상해 보입니다. 특히 일부 열거형 멤버가 숫자인 경우입니다.
  • 도구가 타입스크립트를 트랜스파일하지 않고 타입만 제거하는 경우 열거형을 지원하지 않습니다. 아직 흔한 일은 아니지만, 한 가지 눈에 띄는 예는 Node의 현재 내장된 타입스크립트 지원입니다.

문자열의 성능: 염두에 두어야 할 한 가지는 문자열 비교가 일반적으로 숫자나 심볼 비교보다 느리다는 것입니다. 따라서 값이 문자열인 열거형이나 열거형 패턴은 더 느립니다. 문자열 리터럴 유니온에도 적용됩니다. 하지만 이러한 성능 비용은 비교를 많이 하는 경우에만 중요합니다.

사용 사례는 무엇일까요? 사용 사례를 살펴보면 결정을 내리는 데 도움이 될 수 있습니다.

  • 원시 값을 가진 상수의 네임스페이스:
    • 원시 값이 숫자나 문자열이면 타입스크립트 열거형을 사용할 수 있습니다.
    • 아쉽게도 숫자 값은 각 멤버가 두 개의 프로퍼티를 생성하기 때문에 좋지 않습니다. 키에서 값으로의 매핑과 역 매핑입니다.
    • 그렇지 않으면(또는 열거형을 사용하고 싶지 않으면) 객체 리터럴을 사용할 수 있습니다.
  • 고유한 값을 가진 사용자 정의 타입:
    • 열거형을 사용하면 타입 안전성을 높이고 키와 값을 반복할 수 있도록 문자열 값을 가져야 합니다.
    • 문자열 리터럴 타입의 유니온은 가볍고 빠른 솔루션입니다. 단점은 타입 안전성이 떨어지고 쉽게 조회할 수 있는 네임스페이스 객체가 없다는 것입니다.
    • 런타임에 문자열 리터럴 값에 접근하려면 Set을 사용하여 구체화할 수 있습니다.
    • 견고하고 약간 장황한 솔루션을 원하면 심볼 프로퍼티 값을 가진 객체 리터럴을 사용할 수 있습니다.
  • 객체 값을 가진 상수의 네임스페이스:
    • 이 사용 사례에는 열거형을 사용할 수 없습니다.
    • 프로퍼티 값이 객체인 객체 리터럴을 사용할 수 있습니다. 이 솔루션의 장점은 망라성을 검사할 수 있다는 것입니다.
    • 열거형 값에 메서드가 있기를 원하면 열거형 클래스를 사용할 수 있습니다. 하지만 망라성을 검사할 수 없습니다.