[TypeScript] enum ๐Ÿ‘‰ literal ํƒ€์ž… ๊ฐˆ์•„ํƒ€๊ธฐ(+class-validator, template literal ํƒ€์ž… ํ™œ์šฉ)

ยท

4 min read

3์ค„ ์š”์•ฝ

  • enum์€ tree-shaking, memory-leak ๋ฌธ์ œ๊ฐ€ ์žˆ๊ณ , ์—ฌ๋Ÿฌ enum์„ ํ•˜๋‚˜์˜ enum์œผ๋กœ ํ•ฉ์น  ์ˆ˜ ์—†๋‹ค
  • class-validator์—์„œ IsEnum์„ ์“ฐ๋ ค๋ฉด, ํƒ€์ž…์ด ์•„๋‹Œ ๊ฐ์ฒด๊ฐ€ ํ•„์š”ํ•˜๋‹ค
  • literal๊ณผ Readonly<Record<K, V>> ์œ ํ‹ธ ํƒ€์ž…์„ ํ™œ์šฉํ•ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ–ˆ๋‹ค

๐Ÿ‘‰ velog์—์„œ ๋ณด๊ธฐ


enum์€ ๋น„์‹ธ๊ณ  ๋ถˆํŽธํ•˜๋‹ค!

๋‹ค๋ฅธ ์–ธ์–ด๋“ค๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ TypeScript๋„ ์—ด๊ฑฐํ˜• ํƒ€์ž…์œผ๋กœ enum์„ ์ œ๊ณตํ•œ๋‹ค

ํ•จ์ˆ˜ ์ธ์ž๋กœ ํŠน์ • string๋งŒ ๋“ค์–ด์™€์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ๋“ฑ ์œ ์šฉํ•˜๊ฒŒ ์“ธ ์ˆ˜ ์žˆ๋‹ค


Tree-shaking, ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ

๊ทธ๋Ÿฌ๋‚˜ Line ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ('TypeScript enum์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒŒ ์ข‹์€ ์ด์œ ๋ฅผ Tree-shaking ๊ด€์ ์—์„œ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.')์—์„œ ์ž˜ ์„ค๋ช…ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ, ์ˆœ์ง„ํ•œ enum ํƒ€์ž…์€ tree-shaking์ด ์•ˆ๋˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ ๋‚ญ๋น„๊นŒ์ง€ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค.


์˜ˆ์ƒ๊ณผ๋Š” ๋‹ค๋ฅธ Type Union

๋ฌด์—‡๋ณด๋‹ค ๊ฐœ์ธ์ ์œผ๋กœ ๊ฐ€์žฅ ๋ถˆํŽธํ•˜๊ฒŒ ๋Š๊ปด์ง€๋Š” ๊ฒƒ์€ ๋‹ค๋ฅธ ํƒ€์ž…๋“ค๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ, enum์€ union์œผ๋กœ ์—ฌ๋Ÿฌ enum์„ ํ•˜๋‚˜๋กœ ํ•ฉ์น  ์ˆ˜ ์—†๋‹ค๋Š” ์ ์ด๋‹ค

์•„๋ž˜ ์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค๋ฌด์—์„œ ๊ตฌํ˜„ ์ค‘์ธ API ํƒ€์ž… ์ผ๋ถ€๋ฅผ ์กฐ๊ธˆ ์ˆ˜์ •ํ•ด์„œ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๊ณ , ์ „ํ˜•์ ์ธ enum์„ ๊ฐ€์ ธ์™”๋‹ค

enum PeriodA {
  DAILY = 'DAILY',
  MONTHLY = 'MONTHLY'
}

enum PeriodCommon {
  WEEKLY = 'WEEKLY',
  QUARTERLY = 'QUARTERLY',
  YEARLY = 'YEARLY'
}

Union Type์„ ์“ฐ๋Š” ๋Œ€๋ถ€๋ถ„์€ ์•„๋ž˜์ฒ˜๋Ÿผ A ํƒ€์ž… ์ผ์ˆ˜๋„ ์žˆ๊ณ , B ํƒ€์ž…์ผ ์ˆ˜๋„ ์žˆ์„ ๋•Œ์ด๋‹ค

type StringOrNumber = string | number;

const strOrNumberLogger = (what: StringOrNumber) => console.log(what);
strOrNumberLogger('asdf');
strOrNumberLogger(123123);


๊ทธ๋Ÿฌ๋‚˜ ์ด ๋„˜์˜ enum์€ ๊ทธ๋ ‡์ง€ ์•Š๋‹ค

PeriodA์™€ PeriodCommon๋ฅผ ํ•ฉ์นœ Periods๋ผ๋Š” enum์„ ๋งŒ๋“ค๊ณ  ์‹ถ์ง€๋งŒ ๊ทธ ๊ณผ์ •์€ ์‰ฝ์ง€ ์•Š๋‹ค

์•„๋ž˜์ฒ˜๋Ÿผ ํƒ€์ž… Union์„ ํ•˜๋ฉด ๋‹น์žฅ์€ ์—๋Ÿฌ๊ฐ€ ๋‚˜์ง„ ์•Š์ง€๋งŒ, ์‹ค์งˆ์ ์œผ๋กœ๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ํƒ€์ž…์ด ๋œ๋‹ค

type Periods = PeriodA | PeriodCommon;

const periodLogger = (period: Periods) => console.log(period);
periodLogger('DAILY'); // error


Intersection์€ ๋”๋”์šฑ ์•ˆ๋œ๋‹ค

๋‘˜ ์‚ฌ์ด์— ๊ณตํ†ต์ ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— never ํƒ€์ž…์ด ๋œ๋‹ค

type Periods = PeriodA & PeriodCommon; // -> never

const periodLogger = (period: Periods) => console.log(period);
periodLogger('DAILY'); // error


ํƒ€์ž… assertion์œผ๋กœ ์—๋Ÿฌ๋ฅผ ์•ˆ๋‚ผ ์ˆ˜๋Š” ์žˆ๋Š”๋ฐ..์•„๋ฌด ์˜๋ฏธ ์—†๋Š” ํƒ€์ž… ์„ ์–ธ์ด ๋˜์–ด ๋ฒ„๋ฆฐ๋‹ค



class-validator์˜ IsEnum์„ ์“ฐ๋ ค๋ฉด ๊ฐ์ฒด๊ฐ€ ํ•„์š”ํ•˜๋‹ค!

๊ฐ€์žฅ ์† ํŽธํ•œ๊ฑด ๋‹จ์ˆœํžˆ literal'๋งŒ' ์“ฐ๋Š” ๊ฑฐ๋‹ค

ํ† ์Šค ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ Template Literal Types๋กœ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์ฝ”๋”ฉํ•˜๊ธฐ์—์„œ ์†Œ๊ฐœํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ,

TypeScript์—๋Š” ๋ฌธ์ž์—ด์„ ๊ฐ€์ง€๊ณ  ๋‹ค์–‘ํ•œ ํƒ€์ž…์„ ๋งŒ๋“ค์–ด ๋‚ผ ์ˆ˜ ์žˆ๋Š” ์žฌ๋ฏธ์žˆ๋Š”(?) ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค


๋‹ค๋งŒ ์œ„์—์„œ ์˜ˆ์‹œ๋กœ ๊ฐ€์ ธ์˜จ Periods๋Š” ๋‹จ์ˆœํžˆ ํ•จ์ˆ˜ ์ธ์ž์˜ ํƒ€์ž…๋งŒ ์ถ”๋ก ํ•˜๋Š”๋ฐ ์“ฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ,

์•„๋ž˜์™€ ๊ฐ™์ด class-validator๋ฅผ ํ™œ์šฉํ•ด API ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์—๋„ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์–ด์•ผํ–ˆ๋‹ค

export class GetChartDto {
  @IsNotEmpty()
  @IsEnum(Periods)
  period: PeriodNames;
}


๋‹น์—ฐํ•œ ๊ฑฐ์ง€๋งŒ, ์•„๋ž˜์™€ ๊ฐ™์ด type์„ ํ•จ์ˆ˜ ์ธ์ž๋กœ ๋„ฃ์„ ์ˆ˜๋Š” ์—†๋‹ค


์œ„ ๋ผ์ธ ๋ธ”๋กœ๊ทธ์—์„œ ๊ฐ€์žฅ ์ถ”์ฒœํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๊ฐ์ฒด๋ฅผ ๊ฐ€์ง€๊ณ  Union ํƒ€์ž…์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค

enum์œผ๋กœ union ๋งŒ๋“ค ๋ฐฉ๋ฒ•์„ ๊ตฌ๊ธ€๋ง('typescript enum union')ํ•˜๋ฉด ๋‚˜์—๊ฒŒ ๊ฐ€์žฅ ๋จผ์ € ๋œจ๋Š” ๊ธ€์ธ

์ •๊ทœํ˜„๋‹˜์˜ enum type ๋Œ€์‹  union type์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ์—์„œ๋„ ๊ทธ ๋ฐฉ์‹์„ ๊ฐœ์„ ํ•˜์—ฌ ์‚ฌ์šฉํ•œ ๊ฒฝํ—˜์„ ๋ณด์—ฌ์ฃผ๊ณ  ์žˆ๋‹ค

์—ฌ๊ธฐ์„œ ํ•ต์‹ฌ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค

const READONLY_๊ฐ์ฒด = ๊ฐ์ฒด as const;
type enumLike = keyof READONLY_๊ฐ์ฒด[keyof typeof READONLY_๊ฐ์ฒด]

์งง๊ณ  ์œ ์šฉํ•œ ์ฝ”๋“œ์ง€๋งŒ, ๋ถ€์กฑํ•œ ๋‚˜์—๊ฒŒ๋Š” ์ด๊ฒŒ ๋ญ˜ ์˜๋ฏธํ•˜๋Š”์ง€ ํ•œ๋ฒˆ์— ํŒŒ์•…ํ•˜๊ธฐ๊ฐ€ ์กฐ๊ธˆ ์–ด๋ ต๋‹ค๊ณ  ๋Š๊ปด์กŒ๋‹ค

๋‹ค๋งŒ Readonly ํƒ€์ž…์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์ ์—์„œ ํžŒํŠธ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ๋‹ค

(์ด๊ฒŒ ๊ผญ ํ•„์š”ํ•œ ๊ฑด์ง€๋Š” ์•„์ง ํ…Œ์ŠคํŠธ๋ฅผ ์•ˆํ•ด๋ดค๋‹ค...)


(์ ์–ด๋„ ๋‚˜์—๊ฒŒ๋Š”) ์ข€๋” ํŽธํ•œ ๋ฐฉ๋ฒ•!

๊ฒฐ๋ก ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค

// periods.ts
type ReadonlyRecord<K extends string, V> = Readonly<Record<K, V>>;

export type PeriodANames = 'DAILY' | 'MONTHLY';
export const PeriodA: ReadonlyRecord<PeriodANames, PeriodANames> = {
  DAILY: 'DAILY',
  MONTHLY: 'MONTHLY',
};

type PeriodCommonNames = 'WEEKLY' | 'QUARTERLY' | 'YEARLY';
const PeriodCommon: ReadonlyRecord<PeriodCommonNames, PeriodCommonNames> = { /** */ }

export type PeriodNames = PeriodANames | PeriodCommonNames;
export const Periods = { ...PeriodA, ...PeriodCommon };


์ปค์Šคํ…€ ์œ ํ‹ธ ํƒ€์ž… ReadonlyRecord

์ด ๋ถ€๋ถ„์€ ๋ฐ˜๋“œ์‹œ ํ•„์š”ํ•œ ๋ถ€๋ถ„์€ ์•„๋‹ˆ๊ณ , ๊ฐœ์ธ์ ์œผ๋กœ ๊ฐ์ฒด๋ฅผ Reaonly๋กœ ๋งŒ๋“œ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„ ๋”ฐ๋กœ ์„ ์–ธํ–ˆ๋‹ค

์‹ค๋ฌด ์ฝ”๋“œ์—์„œ๋Š” ์ข€๋” ํŽธํ•œ ์ฝ”๋”ฉ์„ ์œ„ํ•ด ๋ผ๋ฅผ ๋ถ€๋ ค์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ์„ ์–ธํ–ˆ๋‹ค

๊ฐ์ฒด key, value๊ฐ€ ๋ชจ๋‘ string์ธ ๊ฒฝ์šฐ์—๋Š” ๊ธฐ๋ณธ๊ฐ’ ๋•๋ถ„์— ์ œ๋„ค๋ฆญ์„ ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๊ณ ,

string์€ ์•„๋‹ˆ์ง€๋งŒ key, value๊ฐ€ ๋™์ผํ•œ ํƒ€์ž…์ธ ๊ฒฝ์šฐ๋Š” ํ•˜๋‚˜๋งŒ ์ ์–ด์ฃผ๋ฉด ๋œ๋‹ค

export type ReadonlyRecord<P extends string = string, Q = P> = Readonly<Record<P, Q>>;

export const PeriodA: ReadonlyRecord<PeriodANames> = { /** */ }


literal ํƒ€์ž…; PeriodNames, _RestPeriodNames

๋‹จ์ˆœํ•œ ๋ฌธ์ž์—ด literal ํƒ€์ž…์ด๋‹ค

Union์ด๋ผ๋Š” ์˜๋ฏธ์— ๋งž๊ฒŒ, ๊ด€์‹ฌ์‚ฌ์— ๋”ฐ๋ผ ๋ถ„๋ฆฌ๋œ ํƒ€์ž…๋“ค์„ ํ•˜๋‚˜๋กœ ๋ฌถ๋Š” ๊ฒƒ์ด ์‰ฝ๋‹ค

keyof typeof ๊ฐ์ฒด๋ฅผ ๋Œ€์‹ ํ•˜๊ธฐ ์œ„ํ•ด ํƒ€์ž…์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๋” ์จ์•ผํ•œ๋‹ค๋Š” ๊ฒƒ์ด ๋‹จ์ ์ด๊ธฐ๋Š” ํ•˜์ง€๋งŒ,

์•„๋ž˜ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ž๋™์™„์„ฑ์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ํฌ๊ฒŒ ๋ถˆํŽธํ•จ์„ ๋Š๋ผ์ง„ ์•Š์„์ˆ˜ ์žˆ๋‹ค

์˜คํžˆ๋ ค ํ•˜๋‚˜์˜ ํƒ€์ž…์— ๋„ˆ๋ฌด ๋งŽ์€ ์†์„ฑ์ด ์žˆ์–ด์„œ ํƒ€์ดํ•‘ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ •๋„๋ผ๋ฉด, ํƒ€์ž…์„ ์ถฉ๋ถ„ํžˆ ๋ถ„๋ฆฌํ•˜์ง€ ๋ชปํ•œ ๊ฒŒ ์•„๋‹์ง€ ๊ฒ€ํ† ํ•ด๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™๋‹ค

๋˜ ํ† ์Šค ๋ธ”๋กœ๊ทธ์—์„œ ์†Œ๊ฐœํ•œ ๊ฒƒ์ฒ˜๋Ÿผ, template-literal ํƒ€์ž…๋“ค์„ ์กฐํ•ฉํ•œ ์ƒˆ๋กœ์šด ํƒ€์ž…๋“ค์„ ์„ ์–ธํ•˜๊ธฐ ๋งค์šฐ ํŽธ๋ฆฌํ•ด์ง„๋‹ค

export type MarketNames = 'domestic' | 'overseas';
export type CategoryNames = 'index' | 'stock';

export type DetailChartTypeNames = `${MarketNames}-${CategoryNames}`;
export const DetailChartTypes: ReadonlyRecord<DetailChartTypeNames> = {
  'domestic-index': 'domestic-index',
  'domestic-stock': 'domestic-stock',
  'overseas-index': 'overseas-index',
  'overseas-stock': 'overseas-stock',
};


Readonly ๊ฐ์ฒด๋ฅผ class-validator IsEnum์— ํ™œ์šฉ

// get-chart-dto.ts
export class GetChartDto {
  @IsNotEmpty()
  @IsEnum(Periods)
  period: PeriodNames;
}

literal์„ ํ™œ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•œ(type-safe) Readonly ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ , class-validator์—์„œ ํ™œ์šฉํ•œ๋‹ค

class-validator๊ฐ€ ์•„๋‹ˆ๋”๋ผ๋„, ๊ธฐ์กด enum ์‚ฌ์šฉํ•˜๋“ฏ Periods.DAILY์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค

JS๋กœ Transpile ๋˜์—ˆ์„๋•Œ, ๋‹จ์ˆœ ๊ฐ์ฒด ๋ฆฌํ„ฐ๋Ÿด์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ผ์ธ ๋ธ”๋กœ๊ทธ๋Œ€๋กœ๋ผ๋ฉด tree-shaking๋„ ๊ฐ€๋Šฅํ•˜๋‹ค

ย