Skip to content

Commit 89aec47

Browse files
committed
[Docs] 4.1장 정리
1 parent f3a1b78 commit 89aec47

1 file changed

Lines changed: 376 additions & 1 deletion

File tree

  • CH04_타입_확장하기_좁히기/4.1_타입_확장하기
Lines changed: 376 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,376 @@
1-
<!-- 정리할 내용을 작성해주세요. -->
1+
타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는것을 말함.<br />
2+
기본적으로 ts는 `extends`, `교차 타입`, `유니온 타입`을 사용해서 타입을 확장함.
3+
4+
<br />
5+
6+
### 타입 확장의 장점
7+
---
8+
`타입 확장`의 가장 큰 장점은 코드 중복을 줄일 수 있다는 점임. ts를 사용하다보면 필연적으로 중복코드가 생길 수 밖에 없는데 <br />
9+
이때 타입 확장을 통해서 불필요한 중복을 줄일 수 있음.
10+
```ts
11+
// interface 버전
12+
interface BaseMenuItem {
13+
itemName: string | null;
14+
itemImageUrl: string | null;
15+
}
16+
17+
interface BaseCartItem extends BaseMenuItem {
18+
count: number;
19+
}
20+
21+
// 타입 별칭(type) 버전
22+
type BaseMenuItem = {
23+
itemName: string | null;
24+
itemImageUrl: string | null;
25+
};
26+
27+
type BaseCartItem = {
28+
count: number;
29+
} & BaseMenuItem;
30+
```
31+
-> 이처럼 `확장`을 사용하면 불필요한 중복을 줄일 수 있음.
32+
33+
`타입 확장`은 이름처럼 **확장성**이란 장점을 가지고 있음. 관련된 요구사항이 늘어날 때마다 필요한 부분만 추가한 뒤 확장을 시키면 손쉽게 타입을 만들 수 있음.
34+
```ts
35+
interface BaseMenuItem {
36+
itemName: string | null;
37+
itemImageUrl: string | null;
38+
}
39+
40+
interface BaseCartItem extends BaseMenuItem {
41+
count: number;
42+
}
43+
44+
interface EventCartItem extends BaseCartItem {
45+
orderable: boolean;
46+
}
47+
```
48+
또한 기존 `BaseMenuItem`에 변경사항이 생겨도 `BaseCartItem``EventCartItem`은 수정하지 않고, `BaseMenuItem`만 수정하면 되기 때문에 효율적임.
49+
50+
<br />
51+
52+
### 유니온 타입
53+
---
54+
유니온 타입은 **2개 이상의 타입을 조합하여 사용하는 방법**임.
55+
이를 집합의 관점으로 보면 유니온 타입은 `합집합`으로 해석 가능함. <br />
56+
`type MyUnion = A | B;` <br />
57+
58+
**근데 여기서 주의해야 할 점은 앞에서도 말했듯이, `유니온 타입`으로 선언된 값은 _유니온 타입에 포함된 모든 타입이 공통으로 갖고있는 속성에만 접근이 가능함._**
59+
```ts
60+
interface Person {
61+
name: string;
62+
age: number;
63+
}
64+
65+
interface Dog {
66+
name: string;
67+
tail: boolean;
68+
}
69+
70+
type Animal = Person | Dog;
71+
72+
function foo(animal: Animal) {
73+
animal.tail // ❌ Error!
74+
}
75+
```
76+
77+
> [!IMPORTANT]
78+
> 타입스크립트의 타입을 속성의 집합이 아니라, 값의 집합이라고 생각해야 유니온 타입이 합집합 이라는 개념을 이해할 수 있음.
79+
80+
즉, ***`Animal``Person`또는 `Dog`타입에 해당할 뿐이지, `Person`이면서 `Dog`인것은 아니라는 것*** 인데 이해가 잘 가질 않음. <br />
81+
82+
조금 더 직관적으로 해석 해보자.
83+
```ts
84+
// 최최종
85+
interface A {
86+
name: string;
87+
age: number;
88+
}
89+
90+
interface B {
91+
age: string;
92+
loc: string;
93+
}
94+
95+
type AB = A | B;
96+
```
97+
여기서 `AB`
98+
`{
99+
name: string;
100+
age: number;
101+
}`
102+
또는
103+
`{
104+
age: number;
105+
loc: string;
106+
}`
107+
라는 것이지, `A``B`가 합쳐진
108+
***`{
109+
name: string;
110+
age: number;
111+
loc: string;
112+
}`이 아니라는 것이다!!***
113+
114+
<br />
115+
116+
### 교차 타입
117+
`교차 타입`도 기존 타입을 합쳐서 필요한 모든 기능을 가진 하나의 타입을 만드는 것임.
118+
```ts
119+
type DuckA = {
120+
'뒤뚱뒤뚱': true;
121+
'꽥꽥': true;
122+
};
123+
124+
type DuckB = {
125+
'꽥꽥': true;
126+
'포동포동': true;
127+
}
128+
129+
type DuckC = DuckA & DuckB;
130+
// {
131+
// '뒤뚱뒤뚱': true;
132+
// '꽥꽥': true;
133+
// '포동포동': true;
134+
// }
135+
```
136+
뭔가 이상하다????????????????????? <br/>
137+
교차 타입은 `교집합`이라고 했는데, 결과만 보면 `DuckC``DuckA``DuckB`의 타입을 합친 `합집합`으로 보임.
138+
139+
### 왜이러는 걸까? 🤔
140+
141+
앞서 타입 확장은 ***`기존 타입을 사용해서 새로운 타입을 정의하는것`*** 이라고 했음. <br />
142+
즉, `type DuckC = DuckA & DuckB``DuckA``DuckB`를 사용해서 새로운 `교집합 타입인 DuckC`라는 타입을 만드는 것이라고 해석할 수 있음. <br />
143+
그렇다면 `교집합 타입인 DuckC`는 말 그대로 **'교집합'** 을 만족하는 새로 만들어진 타입이니까 `DuckA``DuckB`를 만족하는 타입이라고 볼 수 있음. <br />
144+
여기서 ts의 **`구조적 타이핑`** 특성을 활용한다면 `DuckA``DuckB`를 모두 만족하는 `교집합 타입`이 되려면
145+
`DuckA``DuckB`의 특성을 모두 가질 수 밖에 없는것임. <br />
146+
147+
***따라서 `DuckC`가 위 예시처럼 합쳐진 타입처럼 되는 것.***
148+
149+
미쵸따 미쵸써 이제 조금 감이 잡힌다
150+
151+
다른 예시를 살펴보자
152+
```ts
153+
interface DeliveryTip {
154+
tip: string;
155+
}
156+
157+
interface StartRating {
158+
rate: number;
159+
}
160+
161+
type Filter = DeliveryTip & StartRating;
162+
163+
const filter: Filter = {
164+
tip: '배달 팁',
165+
rate: 4,
166+
};
167+
```
168+
이 예시도 동일함. <br />
169+
`DeliveryTip``StartRating`은 공통된 특성이 없는데도, `Filter`의 타입은 공집합(`never`타입)이 아닌 두 타입의 프로퍼티를 모두 포함한 타입이 됨.<br />
170+
`교차 타입`을 사용할 때 서로 호환되지 않는 경우도 있음.
171+
```ts
172+
type A = string | number;
173+
type B = boolean | number;
174+
175+
type C = A & B; // number;
176+
```
177+
여기서 `C`는 두 타입을 모두 만족하는 경우에만 유지가 되기 때문에, `number`가 됨.
178+
179+
<br />
180+
181+
### extends와 교차 타입
182+
---
183+
`extends` 키워드를 사용해서 교차 타입을 작성할 수도 있음.
184+
```ts
185+
interface Foo {
186+
name: string;
187+
age: number;
188+
}
189+
190+
interface Bar extends Foo {
191+
location: string;
192+
}
193+
```
194+
`Bar``Foo`를 확장함으로써 `Foo`의 속성을 모두 포함하고 있음. 이는 곧 `Bar``Foo`의 속성을 모두 포함하는 `상위 집합`이 되고, `Foo``Bar``부분집합`이 되는 것. <br />
195+
196+
이를 교차타입의 관점에서 작성해본다면?
197+
```ts
198+
type Foo = {
199+
name: string;
200+
age: number;
201+
}
202+
203+
type Bar = {
204+
location: string;
205+
} & Foo;
206+
```
207+
이렇게 됨.
208+
> [!NOTE]
209+
> `유니온``교차 타입`을 사용해서 만드는 새로운 타입은 `타입 별칭(type)`으로만 선언이 가능함.(`interface`로는 불가)
210+
211+
### `교차 타입``extends`는 완벽하게 일치하지 않음.
212+
아래 예시를 살펴보자
213+
```ts
214+
interface Foo {
215+
name: string;
216+
}
217+
218+
interface Bar extends Foo { // Error!
219+
name: number;
220+
}
221+
```
222+
`extends`를 할때 동일한 속성을 다른 타입으로 선언하려고 하면, 타입이 호환되지 않는다는 에러가 발생함.
223+
<img src="../../assets/CH04/iterface_no_same.png" />
224+
225+
그러나 `교차 타입`은 에러가 발생하지 않음.
226+
```ts
227+
type Foo = {
228+
name: string;
229+
}
230+
231+
type Bar = {
232+
name: number;
233+
} & Foo;
234+
235+
// Bar의 name 타입은 never.
236+
```
237+
`교차 타입`으로 동일한 속성을 다른 타입으로 선언하면 에러는 발생하지 않지만, 같은 속성에 대해 서로 호환되지 않는 타입이 선언되어서 `never`타입이 되버림.
238+
<img src="../../assets/CH04/same_intersection.png" />
239+
240+
<br />
241+
242+
### 배민에서는 타입 확장을 어떻게 사용하는지 맛보기
243+
---
244+
<img src="../../assets/CH04/baemin_menu.PNG" width='300px' />
245+
<br />
246+
247+
위 이미지는 배민 서비스 메뉴 목록임.
248+
이를 바탕으로 `Menu`라는 인터페이스를 표현 해보자
249+
```tsx
250+
interface Menu {
251+
name: string;
252+
image: string;
253+
}
254+
255+
function MainMenu() {
256+
const menuList: Menu[] = [
257+
{ name: '', image: '홈.png' },
258+
{ name: '치킨', image: '치킨.png' },
259+
...
260+
{ name: '도시락', image: '도시락.png' },
261+
];
262+
263+
return (
264+
<ul>
265+
{
266+
menuList.map((menu) => (
267+
<li>
268+
<img src={menu.image} />
269+
<span>{menu.name}</span>
270+
</li>
271+
));
272+
}
273+
</ul>
274+
);
275+
}
276+
```
277+
278+
이때, 아래와 같은 2가지 요구사항이 추가되었다고 가정해보자
279+
- ***특정 메뉴를 길게 누르면 gif파일이 재생되어야 함***
280+
- ***특정 메뉴는 이미지 대신 별도의 텍스트만 노출되어야 함***
281+
282+
이러한 요구사항을 만족하는 타입을 작성하는 방법은 2가지로 생각해볼 수 있음.
283+
```ts
284+
// 1. 타입 내에서 속성 추가
285+
interface Menu {
286+
name: string;
287+
image: string;
288+
gif?: string;
289+
text?: string;
290+
}
291+
292+
// 2. 기존 타입을 확장하는 방법
293+
interface Menu {
294+
name: string;
295+
image: string;
296+
}
297+
298+
interface SpecialMenu extends Menu {
299+
gif: string;
300+
}
301+
302+
interface PackageMenu extends Menu {
303+
text: string;
304+
}
305+
```
306+
307+
<br />
308+
`방법 1``방법 2` 각각 적용해보자
309+
310+
```ts
311+
// 서버에서 내려주는 데이터는 아래와 같다고 가정해보자
312+
313+
const menuList = [
314+
{ name: '', image: '홈.png', },
315+
{ name: '치킨', image: '치킨.png', },
316+
];
317+
318+
const specialMenuList = [
319+
{ name: '돈까스', image: '돈까스.png', gif: '돈까스.gif', },
320+
{ name: '피자', image: '피자.png', gif: '피자.gif', },
321+
];
322+
323+
const packageMenuList = [
324+
{ name: '돈까스', image: '돈까스.png', text: '돈까스 또도가스', },
325+
{ name: '피자', image: '피자.png', text: '피자는 고구마피자', },
326+
];
327+
```
328+
329+
#### 방법 1
330+
```ts
331+
interface Menu {
332+
name: string;
333+
image: string;
334+
gif?: string;
335+
text?: string;
336+
}
337+
338+
menuList: Menu[];
339+
specialMenuList: Menu[];
340+
packageMenuList: Menu[];
341+
```
342+
이렇게 하나의 타입으로 모두 표현이 가능함. 그러나 아래와 같은 문제가 발생할 수 있음.
343+
```ts
344+
specialMenuList.map((menu) => menu.text);
345+
// 실제로 런타임에서는 없는 값이기 때문에 undefined가 나옴
346+
```
347+
348+
#### 방법 2
349+
```ts
350+
interface Menu {
351+
name: string;
352+
image: string;
353+
}
354+
355+
interface SpecialMenu extends Menu {
356+
gif: string;
357+
}
358+
359+
interface PackageMenu extends Menu {
360+
text: string;
361+
}
362+
363+
menuList: Menu[];
364+
specialMenuList: SpecialMenu[];
365+
packageMenuList: PackageMenu[];
366+
```
367+
이렇게 작성하면 프로그램을 실행하지 않고도, `컴파일 타임`에 에러를 감지할 수 있음.
368+
```ts
369+
specialMenuList.map((menu) => menu.text); // ❌ Property 'text' does not exist on type 'SpecialMenu'.
370+
```
371+
<br />
372+
373+
따라서 결과적으로 **한 타입에 무분별하게 속성을 추가해서 사용하는 것 보다 타입을 확장해서 사용하는 것이 좋음.** <br />
374+
타입을 확장해서 분리하면 아래와 같은 이점을 얻을 수 있음.
375+
- ***적절한 네이밍을 통해서 의도를 명확하게 할 수 있음***
376+
- ***코드 작성 단계에서 예기치 못한 버그를 예방할 수 있음***

0 commit comments

Comments
 (0)