|
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