diff --git "a/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seulgi.md" "b/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seulgi.md" index 3beced8..b28af95 100644 --- "a/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seulgi.md" +++ "b/CH03_\352\263\240\352\270\211_\355\203\200\354\236\205/3.3_\354\240\234\353\204\244\353\246\255_\354\202\254\354\232\251\353\262\225/seulgi.md" @@ -1 +1,347 @@ - +# 제네릭 사용법 + +## 1) 함수의 제네릭 + +함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 제네릭을 쓸 수 있다. + +```ts +function ReadOnlyRepository( + target: ObjectType | EntitySchema | string +): Repository { + return getConnection("ro").getRepository(target); +} +``` + +- T 자리에 넣는 타입에 따라 ReadOnlyRepository가 적절하게 사용될 수 있다. +- ``는 현재 "이 함수가 다룰 데이터 모델의 타입"을 의미한다. +- T가 무엇이 될 지는 함수가 호출될 때 결정된다. + +### 위의 코드에서 매개변수를 더 알아보자! + +```ts +target: ObjectType | EntitySchema | string; +``` + +- 매개변수는 세 가지 타입 중 하나를 받을 수 있다. + +1. ObjectType → 일반적으로 클래스 타입 (User, Post 같은 엔티티 클래스) +2. EntitySchema → 스키마 기반 엔티티 정의 +3. string → 엔티티 이름을 문자열로 전달 + +### 제네릭이 없다면 어떻게 사용해야 했을까? + +```ts +function ReadOnlyUserRepository(target: ObjectType): Repository { + return getConnection("ro").getRepository(target); +} + +function ReadOnlyPostRepository(target: ObjectType): Repository { + return getConnection("ro").getRepository(target); +} +``` + +- User, Post, Comment 등 엔티티마다 새로운 함수를 만들어야 한다. +- 타입이 추가될 때마다 함수를 계속 만들어야 해서 유지보수가 어렵다. + +```ts +const userRepo = ReadOnlyRepository(User); +const postRepo = ReadOnlyRepository(Post); +``` + +> 위의 코드처럼 제네릭을 통해 유연하게 사용할 수 있다. + +
+
+ +## 2) 호출 시그니처의 제네릭 + +호출 시그니처는 타입스크립트 함수 타입 문법으로 함수의 매개변수와 반환 타입을 미리 선언하는 것을 말한다. + +호출 시그니처를 사용할 때 제네릭 타입을 **어디에 위치**시키는지에 따라
+타입의 범위와 제네릭 타입을 **언제 구체 타입으로 한정할지**를 결정할 수 있다. + +```ts +interface useSelectPaginationProps { + categoryAtom: RecoilState; + filterAtom: RecoilState; + fetcherFunc: ( + props: CommonListRequest + ) => Promise>>; +} +``` + +- 위의 코드에서 ``를 useSelectPaginationProps의 타입 별칭으로 한정했다. +- ``는 useSelectPaginationProps를 사용할 때 타입을 명시해 제네릭 타입으로 구체 타입을 한정한다. +- 위의 훅을 사용할 때 반환값도 인자에서 쓰는 제네릭 타입과 연관이 있기 때문에 위같이 작성한 케이스다. + +```ts +function useSelectPagination< + T extends CardListContent | CommonProductResponse +>({ + categoryAtom, + filterAtom, + fetcherFunc, +}: useSelectPaginationProps): { + intersectionRef: RefObject; + data: T[]; + //... 사용할 타입 정의 +} { + return { + intersectionRef, + data: swappedData ?? [], + //... + }; +} +``` + +
+
+ +## 3) 제네릭 클래스 + +외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스이다. + +```ts +class Box { + private content: T; + + constructor(content: T) { + this.content = content; + } + + getContent(): T { + return this.content; + } +} + +const stringBox = new Box("Hello"); +console.log(stringBox.getContent()); // "Hello" + +const numberBox = new Box(1); +console.log(numberBox.getContent()); // 1 +``` + +- 클래스 이름 뒤에 타입 매개변수인 ``를 선언해준다. +- ``는 메서드의 매개변수나 반환 타입으로 사용할 수 있다. +- 제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용된다. + +### 특정 메서드만을 대상으로 제네릭을 적용하려면? + +해당 메서드를 제네릭 메서드로 선언하면 된다. + +```ts +class A { + get(value: T): T { + return value; + } +} +``` + +
+
+ +## 4) 제한된 제네릭 + +제한된 제네릭은 타입 매개변수에 대한 제약 조건을 설정하는 기능이다. + +### string 타입으로 제약하는 방법을 알아보자 + +```ts +type AllowedKeys = "name" | "age"; + +type MyType = Exclude extends never + ? Record + : never; +``` + +1. AllowedKeys = "name" | "age" → 허용할 키를 지정한다. +2. T extends string → T는 string 타입이어야 한다. +3. Exclude → T에서 "name"과 "age"를 제외한다. +4. 결과가 never이면 (T가 전부 "name" | "age"에 속하면`) + • { name: string, age: string } 같은 객체 타입을 반환한다. +5. 그렇지 않으면 (T에 "name" | "age"가 아닌 값이 포함되면`) + • never 반환한다. + +### Exclude? + +Exclude 는 제네릭 유틸리티 타입으로 T에서 U를 제거하는 타입이다. + +> 타입 매개변수가 특정 타입으로 묶였을 때(bind) 키를 바운드 타입 매개변수(bounded type parameters)라고 부른다. 위의 코드에서는 string을 키의 상한 한계(upper bound)라고 한다. + +### 상속 받을 수 있는 타입을 알아보자! + +상속받을 수 있는 타입으로는 기본 타입뿐만 아니라 상황에 따라 인터페이스나 클래스도 사용할 수 있고 또한 유니온 타입을 상속해서 선언할 수 있다. + +#### (1) 인터페이스를 상속받는 경우 + +```ts +interface Person { + name: string; +} + +function greet(person: T): string { + return `Hello, ${person.name}`; +} + +const user = { name: "Chu", age: 32 }; + +console.log(greet(user)); // "Hello, Chu!" +console.log(greet({ age: 32 })); // Error! Object literal may only specify known properties, and 'age' does not exist in type 'Person'. +``` + +- T extends Person : T는 Person의 구조를 가져야 한다. +- name 속성이 없을 경우 컴파일 오류가 발생한다. + +#### (2) 클래스를 상속받는 경우 + +```ts +class Chu { + getName(): string { + return "seulgi chu"; + } +} + +class Person extends Chu { + getName(): string { + return "name"; + } +} + +function getNames(person: T): string { + return person.getName(); +} + +const person = new Person(); +console.log(getNames(person)); +console.log(getNames({})); // Error! Argument of type '{}' is not assignable to parameter of type 'Person'. Property 'getName' is missing in type '{}' but required in type 'Person'. +``` + +- T extends Person : T는 Person을 상속받은 클래스여야 한다. + +
+
+ +## 5) 확장된 제네릭 + +제네릭 타입은 여러 타입을 상속받을 수 있으며 타입 매개변수를 여러 개 둘 수 있다. + +```ts + +``` + +> 위의 코드처럼 제약해버리면 제네릭의 유연성을 잃어버린다. + +### 유연성을 잃어버리지 않고, 제약해야 할 떄는 타입 매개변수 + 유니온 타입 상속 선언을 활용하자 + +```ts + +``` + +- 유니온 타입으로 T가 여러 타입을 받게 할 수는 있다. +- 다만 타입 매개변수가 여러 개일 때는 처리할 수 없을 땐 매개변수를 하나 더 추가해 선언한다. + +```ts +type Code = 200 | 400 | 500; + +class MyResponse { + private readonly data: Ok | Err | null; + private readonly statusCode: Code | null; + + constructor(data: Ok | Err | null, statusCode: Code | null) { + this.data = data; + this.statusCode = statusCode; + } + + getData(): Ok | Err | null { + return this.data; + } + + getStatusCode(): Code | null { + return this.statusCode; + } +} + +const success = new MyResponse("success", 200); +const server_err = new MyResponse("server err", 500); +const redirect = new MyResponse("redirect", 300); // Error! Argument of type '300' is not assignable to parameter of type 'Code | null'. +``` + +> Code 타입 외의 값(300)을 넣으면 컴파일 오류가 발생한다. + +
+
+ +## 6) 제네릭 예시 + +제네릭을 가장 많이 활용할 때는 API 응답 값의 타입을 지정할 때이고, 제네릭을 활용해 적절한 타입 추론과 코드 재사용성을 높이고 있다. + +```ts +interface ApiResponse { + data: Data; + statusCode: string; + statusMessage?: string; +} + +const fetchPriceInfo = (): Promise> => { + const priceUrl = "https://"; + + return request({ + method: "GET", + url: priceUrl, + }); +}; + +const fetchOrderInfo = (): Promise> => { + const orderUrl = "https://"; + + return request({ + method: "GET", + url: orderUrl, + }); +}; +``` + +> API 응답 값에 따라 달라지는 data를 제네릭 타입 Data로 선언하고 있다. + +
+
+ +## 7) 제네릭이 굳이 필요 없는 경우 + +### 제네릭을 굳이 사용하지 않아도 되는 타입 + +```ts +type MyType = T; +type OurType = "ME" | "YOU" | "OUR"; + +interface GeneralType { + getTypes(): MyType; +} +``` + +- 위의 MyType이 다른 곳에 사용되지 않고, 함수 반환값으로 쓰이고 있다 가정할 경우 굳이 필요하지 않다. +- 제네릭을 사용하지 않고 타입 매개변수를 그대로 선언하는 것과 같은 기능을 하고 있다. + +```ts +type OurType = "ME" | "YOU" | "OUR"; + +interface GeneralType { + getTypes(): OurType; +} +``` + +> 위와 같은 코드의 동작과 동일하기에 굳이 제네릭 사용이 필요없다. + +### any 사용하기 + +제네릭에 any를 사용하면 제네릭의 장점과 타입 추론 및 타입 검사를 할 수 있는 이점을 누릴 수 없게 된다. + +```ts +type ReturnType = { + // ... +}; +``` + +### 가독성을 고려하지 않은 경우 + +제네릭을 과하게 사용하면 가독성이 해쳐지기 때문에 코드를 읽고 타입을 이해하기 어렵다. diff --git "a/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seulgi.md" "b/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seulgi.md" index 3beced8..9d0e521 100644 --- "a/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seulgi.md" +++ "b/CH04_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260_\354\242\201\355\236\210\352\270\260/4.1_\355\203\200\354\236\205_\355\231\225\354\236\245\355\225\230\352\270\260/seulgi.md" @@ -1 +1,340 @@ - +# 타입 확장하기 + +타입 확장은 기존 타입을 사용해서 새로운 타입을 정의하는 것이다. + +interface, type을 통해 타입을 정의하고, extends, 교차 타입, 유니온 타입을 사용해 타입을 확장한다. + +## 1) 타입 확장의 장점 + +타입 확장의 장점은 코드 중복을 줄일 수 있다는 것이다.
+중복되는 타입을 반복적으로 선언하는 것보다 기존에 작성한 타입을 바탕으로 타입 확장을 함으로써 불필요한 코드 중복을 줄일 수 있다. + +```ts +interface MenuItem { + name: string | null; + imageUrl: string | null; + discount: number; + stock: number | null; +} + +interface CartItem extends MenuItem { + quantity: number; +} +``` + +> 위의 코드는 메뉴 타입 기준으로 타입 확장을 해서 장바구니 타입을 정의한 것이다.
+> 장바구니 요소는 메뉴 요소가 가지는 모든 타입이 필요하다. 해당 코드는 확장을 통해 중복 코드를 줄여준다.
+> 장바구니 요소가 메뉴 요소의 확장되었다는 것이 쉽게 확인되어 좀 더 명시적인 코드를 작성할 수 있게 해준다. + +### interface를 type으로 바꿔보자. + +```ts +type MenuItem = { + name: string | null; + imageUrl: string | null; + discount: number; + stock: number | null; +}; + +type CartItem = MenuItem & { + quantity: number; +}; +``` + +> 타입 확장은 중복 제거, 명시적 코드 작성 외에도 확장성이란 장점이 있다. + +### 요구 사항이 계속 늘어난다면 새로운 타입을 확장해 정의할 수 있다. + +수정할 수 있는 장바구니 요소 타입으로 "품절 여부, 수정 옵션 배열 정보"가 추가되었다고 가정하자. + +```ts +interface EditableCartItem extends CartItem { + isSoldOut: boolean; + optionGroups: SelectableOptionGroup[]; +} + +interface EventCartItem extends CartItem { + orderable: boolean; +} +``` + +> 위의 코드처럼 장바구니와 관련된 요구 사항이 생길 때마다 필요한 타입을 손쉽게 만들 수 있다.
+> 기존 장바구니 요소에 대한 요구 사항이 바뀌어도 CartItem 타입만 수정하면 된다.(효울적 👏) + +
+
+ +## 2) 유니온 타입 + +유니온 타입은 집합 관점에서 본다면 합집합이다. + +```ts +type MyUnion = A | B; +``` + +- A와 B의 유니온 타입인 MyUnion은 A, B의 합집합이다. +- 집합 A의 모든 원소는 집합 MyUnion의 원소이며 집합 B 모든 원소 역시 MyUnion의 원소라는 뜻이다. +- 다만, **유니온 타입으로 선언된 값은 유니온 타입에 포함된 모든 타입이 공통으로 갖고 있는 속성에만 접근할 수 있다.** + +```ts +interface Order { + orderId: string; + price: number; +} + +interface Delivery { + orderId: string; + time: number; + distance: string; +} + +function getDeliveryDistance(step: Order | Delivery) { + return step.distance; +} // Property 'distance' does not exist on type 'Order | Delivery'. Property 'distance' does not exist on type 'Order'. +``` + +> 함수 본문에서 step.distance를 호출하고 있는데 distance는 Delivery에만 있는 속성이기 때문에
+> step이 Order일 경우엔 해당 속성을 찾을 수 없어 에러가 발생한다.
+> 즉, step이라는 유니온 타입은 두 타입에 해당할 뿐이지 Order이면서 Delivery인 것은 아니다. + +### 타입스크립트 타입을 속성의 집합이 아닌 값의 집합으로 생각해야
유니온 타입이 합집합이라는 개념을 이해할 수 있다. + +```ts +type A = "A" | "B"; +type B = "B" | "C"; + +type AB = A | B; // "A" | "B" | "C"; + +type Cat = { type: "cat"; meow: () => void }; +type Dog = { type: "dog"; bark: () => void }; +type Animal = Cat | Dog; // { type: "cat"; meow: () => void } 또는 { type: "dog"; bark: () => void }; +``` + +> 아래 잘못된 예시를 보자! + +```ts +type Cat = { type: "cat"; sound: () => void }; +type Dog = { type: "dog"; sound: string }; +type Animal = Cat | Dog; + +const animal: Animal = { type: "cat", sound: () => console.log("meow") }; +const animal2: Animal = { type: "cat", sound: "bark!" }; // Type '{ type: "cat"; sound: string; }' is not assignable to type 'Animal'. Types of property 'sound' are incompatible. Type 'string' is not assignable to type '() => void'. +const animal3: Animal = { type: "dog", sound: () => console.log("bark!") }; // Type '{ type: "dog"; sound: () => void; }' is not assignable to type 'Animal'. Types of property 'sound' are incompatible. Type '() => void' is not assignable to type 'string'. +``` + +- 위의 예시처럼 Cat과 Dog 두 타입에 있는 sound 속성의 타입은 () => void | string처럼 자동으로 변환되지 않는다. +- 유니온 타입에서는 각 속성이 완전히 일치해야 한다. + +
+
+ +## 3) 교차 타입 + +교차 타입도 기존 타입을 합쳐 필요한 모든 기능을 가진 하나의 타입을 만드는 것으로 이해할 수 있다. + +```ts +interface Order { + orderId: string; + price: number; +} + +interface Delivery { + orderId: string; + time: number; + distance: string; +} + +type Progress = Order & Delivery; + +function getProgressInfo(progress: Progress) { + console.log(progress.price); + console.log(progress.distance); +} +``` + +> 유니온 타입과 다른 점으로는 Progress 타입은 Order과 Delivery 타입을 합쳐 모든 속성을 가진 단일 타입이 된다. 따라서, progress 값은 Order과 Delivery 타입의 속성을 포함하고 있다. + +### 교차 타입의 개념을 다시 짚고 넘어가자. + +```ts +type MyIntersection = A & B; +``` + +- 교차 타입은 교집합의 개념과 비슷하다. +- MyIntersection 타입의 모든 값은 A 타입 값 + B 타입 값 + +> 집합의 관점에서 보면 MyIntersection의 모든 원소는 집합 A의 원소이자 집합 B의 원소이다. + +### 다른 예시를 살펴보자. + +```ts +interface Dog { + type: "dog"; +} +interface Cat { + sound: () => void; +} +type Animal = Cat & Dog; + +const animal: Animal = { type: "dog", sound: () => console.log("bark") }; +``` + +- 교차 타입은 두 타입의 교집합을 의미하는데 Dog, Cat 타입에 공통 속성이 없어도 Animal 타입은 공집합(never)가 아닌 모두 포함한 타입이다. +- 교차 타입 Animal은 Dog의 type 속성과 Cat의 sound 속성을 모두 만족하는 값이 된다. + +> 왜냐하면, 타입이 속성이 아닌 값의 집합으로 해석되기 때문이다. 다만 교차 타입을 사용할 때 타입이 서로 호환되지 않는 경우도 있다. + +```ts +type MyId = string | number; +type MyNumber = number | boolean; + +type MyInfo = MyId & MyNumber; +``` + +### 위의 MyInfo 타입을 뭘까? + +1. string이면서 number +2. string이면서 boolean +3. number이면서 number +4. number이면서 boolean + +> MyInfo 타입은 MyId & MyNumber 교차 타입이므로 두 타입을 모두 만족하는 경우만 유지된다. 따라서 3번인 number 타입이 된다. + +
+
+ +## 4) extends와 교차 타입 + +extends 키워드를 사용해서 교차 타입을 작성할 수 있다. + +```ts +type MenuItem = { + name: string | null; + imageUrl: string | null; + discount: number; + stock: number | null; +}; + +type CartItem = MenuItem & { + quantity: number; +}; +``` + +- 유니온 타입과 교차 타입을 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있다. + +### extends 키워드를 사용한 타입이 교차 타입과 100% 상응하지 않는다?! + +```ts +interface A { + a: string; +} + +interface B extends A { + a: number; +} + +// Interface 'B' incorrectly extends interface 'A'. +// Types of property 'a' are incompatible. +// Type 'number' is not assignable to type 'string'. +``` + +> extends 키워드로 확장한 B타입에 number 타입의 a 속성을 선언하면 a 타입이 호환되지 않는다는 에러가 발생한다. + +```ts +type A = { + a: string; +}; + +type B = A & { + a: number; +}; +``` + +- 에러는 발생하지 않지만, a속성의 타입은 never다. +- type 키워드는 교차 타입으로 선언되면 새롭게 추가되는 속성에 대해 미리 알 수가 없다. + +> 그래서, 에러는 발생하지 않지만 같은 속성에 대해 서로 호횐되지 않은 타입이라 never 타입이 된다. + +
+
+ +## 5) 배달의 민족 메뉴 시스템에 타입 확장 적용하기 + +```ts +// 메뉴에 대한 타입 : 메뉴명, 이미지 정보가 있다. +interface Menu { + name: string; + image: string; +} + +function MainMenu() { + const menuList: Menu[] = [{ name: "중식", image: "중식.png" }, ...]; + + return ( +
    + {menuList.map((menu) => +
  • + + {menu.name} +
  • )} +
+ ) +} +``` + +### 특정 메뉴의 중요도를 다르게 주기 위한 요구 사항이 추가되었다면? + +1. 특정 메뉴를 누르면 gif 재생되어야 한다. +2. 특정 메뉴 이미지 대신 텍스트만 노출되어야 한다. + +```ts +// 방법 1: 타입 내 속성을 추가한다. +interface Menu { + name: string; + image: string; + gif?: string; + text?: string; +} + +// 방법 2: 타입을 확장한다. +interface SpecialMenu extends Menu { + gif: string; +} +interface TextMenu extends Menu { + text: string; +} +``` + +### 방법 1: 하나의 타입에 여러 속성을 추가할 때 + +```ts +const menuList: Menu[] = [...]; +const specialMenuList: Menu[] = [...]; +const textMenuList: Menu[] = [...]; +``` + +- 각 메뉴 목록을 모두 Menu[]로 표현할 수 있다. +- 다만, specialMenuList 배열의 원소가 각 속성에 접근할 때 아래와 같은 문제가 생길 수 있다. + +```ts +specialMenuList.map((menu) => menu.text); // TypeError! +``` + +- specialMenuList는 Menu 타입의 원소를 갖기 때문에 text 속성에 접근할 수 있지만, text 속성을 가지지 않아 에러가 난다. + +### 방법 2: 타입을 확장하는 방식 + +각 배열의 타입을 확장할 타입에 맞게 명확히 규정할 수 있다. + +```ts +const menuList: Menu[] = [...]; +const specialMenuList: SpecialMenu[] = [...]; +const textMenuList: TextMenu[] = [...]; + +specialMenuList.map((menu) => menu.text); // Property "text" does not exist on type "SpecialMenu" +``` + +- specialMenuList는 text 속성 자체가 없어 바로 에러를 알 수 있다. + +> 결과적으로 주어진 타입에 무분별하게 속성을 추가하여 사용하는 것보다 타입을 확장해서 사용하는 것이 좋다. +> 위의 예시처럼 적절한 네이밍으로 사용해 타입 의도를 명확히 할 수 있고, 코드 작성 단계에서 예기치 못한 버그 예방도 가능하다.