[TypeScript] enum πŸ‘‰ literal νƒ€μž… κ°ˆμ•„νƒ€κΈ°(+class-validator, template literal νƒ€μž… ν™œμš©)

gitgitWi's TILΒ·2022λ…„ 1μ›” 11일
3
post-thumbnail

3쀄 μš”μ•½

  • enum은 tree-shaking, memory-leak λ¬Έμ œκ°€ 있고, μ—¬λŸ¬ enum을 ν•˜λ‚˜μ˜ enum으둜 ν•©μΉ  수 μ—†λ‹€
  • class-validatorμ—μ„œ IsEnum을 μ“°λ €λ©΄, νƒ€μž…μ΄ μ•„λ‹Œ 객체가 ν•„μš”ν•˜λ‹€
  • literalκ³Ό Readonly<Record<K, V>> μœ ν‹Έ νƒ€μž…μ„ ν™œμš©ν•΄ 문제λ₯Ό ν•΄κ²°ν–ˆλ‹€

πŸ‘‰ hashnodeμ—μ„œ 보기


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도 κ°€λŠ₯ν•˜λ‹€

profile
κ°€λ³κ²Œ TIL λ‚¨κΈ°λŠ” velog, κΎΈμ€€νžˆ μ„±μž₯ν•˜λŠ” 개발자

1개의 λŒ“κΈ€

comment-user-thumbnail
2024λ…„ 3μ›” 20일

κ΄€λ ¨ λ¦¬μ†ŒμŠ€λ“€μ„ 많이 μ œκ³΅ν•΄μ£Όμ…”μ„œ 정말 도움이 많이 λ˜μ—ˆμ–΄μš”.
그런데, λ°±μ—”λ“œμ—μ„œ νŠΈλ¦¬μ‰μ΄ν‚Ή κ΄€μ μ—μ„œμ˜ enumνƒ€μž…μ„ μ‚¬μš©ν•˜μ§€ μ•Šμ•„μ•Ό ν•  μ΄μœ κ°€ μžˆμ„κΉŒμš”?

λ‹΅κΈ€ 달기