Skip to content

Commit 5848d19

Browse files
authored
Merge pull request #18 from gitgitWi/nextjs/v1
포스팅 (`enum -> literal 타입 갈아타기`) 추가, prettier/editorconfig 설정 추가
2 parents 1337e2f + 194198d commit 5848d19

File tree

5 files changed

+574
-9
lines changed

5 files changed

+574
-9
lines changed

.editorconfig

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# http://editorconfig.org
2+
root = true
3+
4+
[*]
5+
indent_style = space
6+
indent_size = 2
7+
end_of_line = lf
8+
charset = utf-8
9+
trim_trailing_whitespace = true
10+
insert_final_newline = true
11+
12+
[*.json]
13+
insert_final_newline = ignore
14+
15+
[**.min.js]
16+
indent_style = ignore
17+
insert_final_newline = ignore
18+
19+
[*.md]
20+
trim_trailing_whitespace = false
21+

.prettierrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"tabWidth": 2,
3+
"useTabs": false,
4+
"endOfLine": "lf",
5+
"printWidth": 90,
6+
"trailingComma": "es5"
7+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"eslint-plugin-prettier": "^3.4.0",
3030
"eslint-plugin-react": "^7.24.0",
3131
"prettier": "^2.3.2",
32+
"typed-scss-modules": "^4.1.1",
3233
"typescript": "^4.3.5"
3334
}
3435
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# [TypeScript] `enum` -> `literal` 타입 갈아타기(+`class-validator`, `template literal` 타입 활용)
2+
3+
### 3줄 요약
4+
5+
> - `enum`은 tree-shaking, memory-leak 문제가 있고, 여러 enum을 하나의 enum으로 합칠 수 없다
6+
> - `class-validator`에서 `IsEnum`을 쓰려면, 타입이 아닌 객체가 필요하다
7+
> - `literal``Readonly<Record<K, V>>` 유틸 타입을 활용해 문제를 해결했다
8+
9+
[👉 hashnode에서 보기](https://wiii.hashnode.dev/enum-to-template-literal)
10+
11+
---
12+
13+
## `enum`은 비싸고 불편하다!
14+
15+
다른 언어들과 마찬가지로 `TypeScript`도 열거형 타입으로 [`enum`](https://www.typescriptlang.org/ko/docs/handbook/enums.html)을 제공한다
16+
17+
함수 인자로 특정 string만 들어와야 하는 경우 등 유용하게 쓸 수 있다
18+
19+
<br />
20+
21+
### Tree-shaking, 메모리 문제
22+
23+
그러나 Line 기술 블로그(['TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.'](https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/))에서 잘 설명하는 것처럼, 순진한 enum 타입은 tree-shaking이 안되고 메모리 낭비까지 이어질 수 있다.
24+
25+
<br />
26+
27+
### 예상과는 다른 Type Union
28+
29+
무엇보다 개인적으로 가장 불편하게 느껴지는 것은 다른 타입들과는 다르게, ***enum은 `union`으로 여러 enum을 하나로 합칠 수 없다는 점이다***
30+
31+
아래 예시 코드는 실무에서 구현 중인 API 타입 일부를 조금 수정해서 가져온 것이고, 전형적인 enum을 가져왔다
32+
33+
```typescript
34+
enum PeriodA {
35+
DAILY = 'DAILY',
36+
MONTHLY = 'MONTHLY'
37+
}
38+
39+
enum PeriodCommon {
40+
WEEKLY = 'WEEKLY',
41+
QUARTERLY = 'QUARTERLY',
42+
YEARLY = 'YEARLY'
43+
}
44+
```
45+
46+
[Union Type](https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html#%EC%9C%A0%EB%8B%88%EC%96%B8-%ED%83%80%EC%9E%85)을 쓰는 대부분은 아래처럼 A 타입 일수도 있고, B 타입일 수도 있을 때이다
47+
48+
```typescript
49+
type StringOrNumber = string | number;
50+
51+
const strOrNumberLogger = (what: StringOrNumber) => console.log(what);
52+
strOrNumberLogger('asdf');
53+
strOrNumberLogger(123123);
54+
```
55+
56+
<br />
57+
58+
**_그러나 이 넘의 enum은 그렇지 않다_**
59+
60+
`PeriodA``PeriodCommon`를 합친 `Periods`라는 enum을 만들고 싶지만 그 과정은 쉽지 않다
61+
62+
아래처럼 타입 Union을 하면 당장은 에러가 나진 않지만, 실질적으로는 사용할 수 없는 타입이 된다
63+
64+
```typescript
65+
type Periods = PeriodA | PeriodCommon;
66+
67+
const periodLogger = (period: Periods) => console.log(period);
68+
periodLogger('DAILY'); // error
69+
```
70+
71+
<br />
72+
73+
Intersection은 더더욱 안된다
74+
75+
둘 사이에 공통점이 없기 때문에 **`never`** 타입이 된다
76+
77+
```typescript
78+
type Periods = PeriodA & PeriodCommon; // -> never
79+
80+
const periodLogger = (period: Periods) => console.log(period);
81+
periodLogger('DAILY'); // error
82+
```
83+
84+
<br />
85+
86+
타입 assertion으로 에러를 안낼 수는 있는데..***아무 의미 없는 타입 선언***이 되어 버린다
87+
88+
![](https://images.velog.io/images/johnwi/post/669de5ad-34fb-4a23-b86e-a8cb8ea5326a/image.png)
89+
90+
<br />
91+
92+
---
93+
94+
## `class-validator``IsEnum`을 쓰려면 객체가 필요하다!
95+
96+
가장 속 편한건 단순히 `literal`'만' 쓰는 거다
97+
98+
토스 기술 블로그 [Template Literal Types로 타입 안전하게 코딩하기](https://toss.tech/article/template-literal-types)에서 소개하는 것처럼,
99+
100+
TypeScript에는 문자열을 가지고 다양한 타입을 만들어 낼 수 있는 재미있는(?) 기능이 있다
101+
102+
<br />
103+
104+
다만 위에서 예시로 가져온 `Periods`는 단순히 함수 인자의 타입만 추론하는데 쓰는 것이 아니라,
105+
106+
아래와 같이 **[`class-validator`](https://github.com/typestack/class-validator)를 활용해 API 요청 유효성 검사**에도 사용될 수 있어야했다
107+
108+
```typescript
109+
export class GetChartDto {
110+
@IsNotEmpty()
111+
@IsEnum(Periods)
112+
period: PeriodNames;
113+
}
114+
```
115+
116+
<br />
117+
118+
당연한 거지만, 아래와 같이 type을 함수 인자로 넣을 수는 없다
119+
120+
![](https://images.velog.io/images/johnwi/post/515a911a-181d-4730-8f15-610f6dc508a3/image.png)
121+
122+
123+
<br />
124+
125+
위 라인 블로그에서 가장 추천하는 방법은 객체를 가지고 Union 타입을 만드는 것이다
126+
127+
enum으로 union 만들 방법을 구글링(`'typescript enum union'`)하면 나에게 가장 먼저 뜨는 글인
128+
129+
정규현님의 [enum type 대신 union type으로 변경하기](https://ajdkfl6445.gitbook.io/study/typescript/enum-type-union-type)에서도 그 방식을 개선하여 사용한 경험을 보여주고 있다
130+
131+
여기서 핵심은 아래와 같다
132+
133+
```typescript
134+
const READONLY_객체 = 객체 as const;
135+
type enumLike = keyof READONLY_객체[keyof typeof READONLY_객체]
136+
```
137+
138+
짧고 유용한 코드지만, 부족한 나에게는 이게 뭘 의미하는지 한번에 파악하기가 조금 어렵다고 느껴졌다
139+
140+
다만 `Readonly` 타입을 사용한다는 점에서 힌트를 얻을 수 있었다
141+
142+
_(이게 꼭 필요한 건지는 아직 테스트를 안해봤다...)_
143+
144+
<br />
145+
146+
## _(적어도 나에게는)_ 좀더 편한 방법!
147+
148+
결론은 아래와 같다
149+
150+
```typescript
151+
// periods.ts
152+
type ReadonlyRecord<K extends string, V> = Readonly<Record<K, V>>;
153+
154+
export type PeriodANames = 'DAILY' | 'MONTHLY';
155+
export const PeriodA: ReadonlyRecord<PeriodANames, PeriodANames> = {
156+
DAILY: 'DAILY',
157+
MONTHLY: 'MONTHLY',
158+
};
159+
160+
type PeriodCommonNames = 'WEEKLY' | 'QUARTERLY' | 'YEARLY';
161+
const PeriodCommon: ReadonlyRecord<PeriodCommonNames, PeriodCommonNames> = { /** */ }
162+
163+
export type PeriodNames = PeriodANames | PeriodCommonNames;
164+
export const Periods = { ...PeriodA, ...PeriodCommon };
165+
```
166+
167+
<br />
168+
169+
### 커스텀 유틸 타입 `ReadonlyRecord`
170+
171+
이 부분은 반드시 필요한 부분은 아니고, 개인적으로 객체를 `Reaonly`로 만드는 경우가 많아 따로 선언했다
172+
173+
실무 코드에서는 좀더 편한 코딩을 위해 끼를 부려서 아래와 같이 선언했다
174+
175+
객체 `key`, `value`가 모두 `string`인 경우에는 기본값 덕분에 제네릭을 생략할 수 있고,
176+
177+
`string`은 아니지만 `key`, `value`가 동일한 타입인 경우는 하나만 적어주면 된다
178+
179+
```typescript
180+
export type ReadonlyRecord<P extends string = string, Q = P> = Readonly<Record<P, Q>>;
181+
182+
export const PeriodA: ReadonlyRecord<PeriodANames> = { /** */ }
183+
```
184+
185+
<br />
186+
187+
### `literal` 타입; `PeriodNames`, `_RestPeriodNames`
188+
189+
단순한 문자열 literal 타입이다
190+
191+
Union이라는 의미에 맞게, ***관심사에 따라 분리된 타입들을 하나로 묶는 것이 쉽다***
192+
193+
`keyof typeof 객체`를 대신하기 위해 타입을 하나하나 더 써야한다는 것이 단점이기는 하지만,
194+
195+
아래 객체를 생성할 때 자동완성이 되기 때문에 크게 불편함을 느끼진 않을수 있다
196+
197+
오히려 하나의 타입에 너무 많은 속성이 있어서 타이핑 시간이 오래 걸릴 정도라면, 타입을 충분히 분리하지 못한 게 아닐지 검토해봐야 할 것 같다
198+
199+
또 토스 블로그에서 소개한 것처럼, **`template-literal` 타입들을 조합한 새로운 타입들을 선언하기 매우 편리해진다**
200+
201+
```typescript
202+
export type MarketNames = 'domestic' | 'overseas';
203+
export type CategoryNames = 'index' | 'stock';
204+
205+
export type DetailChartTypeNames = `${MarketNames}-${CategoryNames}`;
206+
export const DetailChartTypes: ReadonlyRecord<DetailChartTypeNames> = {
207+
'domestic-index': 'domestic-index',
208+
'domestic-stock': 'domestic-stock',
209+
'overseas-index': 'overseas-index',
210+
'overseas-stock': 'overseas-stock',
211+
};
212+
```
213+
214+
215+
<br />
216+
217+
### Readonly 객체를 `class-validator IsEnum`에 활용
218+
219+
```typescript
220+
// get-chart-dto.ts
221+
export class GetChartDto {
222+
@IsNotEmpty()
223+
@IsEnum(Periods)
224+
period: PeriodNames;
225+
}
226+
```
227+
228+
`literal`을 활용하여 안전한(type-safe) `Readonly` 객체를 만들고, `class-validator`에서 활용한다
229+
230+
`class-validator`가 아니더라도, 기존 `enum` 사용하듯 `Periods.DAILY`와 같이 사용할 수도 있다
231+
232+
JS로 Transpile 되었을때, 단순 객체 리터럴이기 때문에 라인 블로그대로라면 tree-shaking도 가능하다

0 commit comments

Comments
 (0)