[TypeScript] enum ๐ literal ํ์ ๊ฐ์ํ๊ธฐ(+class-validator, template literal ํ์ ํ์ฉ)
3์ค ์์ฝ
enum
์ tree-shaking, memory-leak ๋ฌธ์ ๊ฐ ์๊ณ , ์ฌ๋ฌ enum์ ํ๋์ enum์ผ๋ก ํฉ์น ์ ์๋คclass-validator
์์IsEnum
์ ์ฐ๋ ค๋ฉด, ํ์ ์ด ์๋ ๊ฐ์ฒด๊ฐ ํ์ํ๋คliteral
๊ณผReadonly<Record<K, V>>
์ ํธ ํ์ ์ ํ์ฉํด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค
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๋ ๊ฐ๋ฅํ๋ค