제네릭이란
- 제네릭이란 함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동작하도록 만들어주는 타입스크립트의 기능이다.
제네릭이 필요한 상황
- 다음과 같이 다양한 타입의 매개변수를 받고 해당 매개변수를 그대로 반환하는 함수가 하나 필요하다고 가정해보자.

- 다양한 타입의 매개변수를 제공받아야 하기 때문에 매개변수 value의 타입을 일단 any 타입으로 했다. 물론 unknown으로 정의해도 괜찮다.
- 이 함수는 인수로 전달한 값을 그냥 그대로 반환하는 단순한 함수이다.
- 따라서 변수 num에는 10이 저장되고, 변수 str에는 “string”이 저장된다. 그런데 현재 num과 str의 타입은 any 타입이 된다. func 함수의 반환값 타입이 return 문을 기준으로 추론되었기 때문이다.
- 이렇게 함수 호출 결과를 저장하는 num, str 등의 변수가 any 타입으로 추론되면 다음과 같은 문제점이 발생한다.

- num에는 분명 Number 타입의 값 10이 저장되어 있지만, any 타입으로 추론되어 버렸기 때문에 toUpperCase 등의 String 타입의 메서드를 사용해도 타입스크립트가 오류를 감지하지 못한다.
- 결국 실제로 실행하면 런타임 오류를 발생시킬 것이다.
- 이번에는 매개변수의 타입을 any 타입 말고 unknown 타입으로 정의해보자.

- 그러면 toUpperCase 같은 메서드 호출은 방지할 수 있다.
- 그러나 이번에는 또 다른 문제가 발생한다. toFixed 같은 Number 타입의 메서드 호출도 함께 오류로 판단한다.
- 왜냐하면 unknown 타입은 어떤 연산, 어떤 메서드도 할 수 없는 전체집합이잖아.
- 따라서 num에 10이 저장될 것이 분명한데도 이 값을 사용하려면 다음과 같이 비효율적으로 타입 좁히기를 이용해야 한다.

- 그냥 인수로 Number 타입의 값을 전달하면 반환 타입이 Number가 되고, 인수로 String 타입의 값을 전달하면 반환값의 타입도 String 타입이 되었으면 좋겠을때, 제네릭을 사용하면 된다.
제네릭(Generic) 함수
- 제네릭(Generic) 함수란 무엇일까? 일단 제네릭이란 일반적인 또는 포괄적인 이라는 뜻을 갖고 있다. 따라서 제네릭 함수는 일반적인 함수 또는 포괄적인 함수 정도로 해석할 수 있다.
- 따라서 '제네릭 함수는 두루두루 모든 타입의 값을 다 적용할 수 있는 그런 범용적인 함수이다' 정도로 이해할 수 있다.

- 함수 이름 뒤에 꺽쇠를 열고 타입을 담는 변수인 타입 변수 T를 선언한다. 그리고 매개변수와 반환값의 타입을 이 타입변수 T로 설정한다.
- T에 어떤 타입이 할당될 지는 함수가 호출될 때 결정된다.
- func(10) 처럼 Number 타입의 값을 인수로 전달하면 매개변수 value에 Number 타입의 값이 저장되면서 T가 Number 타입으로 추론된다. 이때 T가 Number 타입으로 추론된다. 그럼 이때의 func 함수의 반환값 타입또한 Number 타입이 된다.

- 제네릭 함수를 호출할 때 다음과 같이 타입 변수에 할당할 타입을 직접 명시하는 것도 가능하다.

- 위 코드의 흐름은 다음과 같다.
- T에
[Number, Number, Number] 튜플 타입이 할당된다.
- 매개변수 value와 반환값 타입이 모두 튜플 타입이 된다.
- 만약 위 코드에서 타입 변수에 할당할 타입을 튜플 타입으로 설정하지 않았다면 T가 number[] 타입으로 추론 되었을 것이다.
- 타입스크립트는 타입을 추론할 때 항상 일반적이고 좀 더 범용적인 타입으로 추론하기 때문이다.
- 이렇듯 타입 변수에 할당하고 싶은 특정 타입이 존재한다면 함수 호출과 함께 꺽쇠를 열고 직접 명시해주는게 좋다.
- 그렇지 않은 대다수의 상황에서는 알아서 잘 추론되기 때문에 굳이 타입 변수를 설정하지 않아도 된다.
타입 변수 응용하기
사례 1.
- 만약 2개의 타입 변수가 필요한 상황이라면, 다음과 같이 T, U 처럼 2개의 타입 변수를 사용해도 된다.

- 따라서 위 코드에서 T는 String 타입으로 U는 Number 타입으로 추론된다.
사례 2.
- 다양한 배열 타입을 인수로 받는 제네릭 함수를 만들어야 한다면 다음과 같이 할 수 있다.

- 함수 매개변수 data의 타입을
T[]로 설정했기 때문에 배열이 아닌 값은 인수로 전달할 수 없다. 배열을 인수로 전달하면 T는 배열의 요소 타입으로 할당된다.
- 첫번째 호출에서는 인수로
Number[] 타입의 값을 전달했으므로 이때의 T는 Number 타입으로 추론된다. 이때의 함수 반환값 타입은 Number 타입이 된다.
- 두번째 호출에서는 인수로
(String | Number)[] 타입의 값을 전달했으므로 이때의 T는 String | Number 타입으로 추론된다. 이때의 함수 반환값 타입은 String | Number 타입이 된다.
사례 3.
- 그런데 위 사례에서 만약 반환값의 타입을 배열의 첫번째 요소의 타입이 되도록 하려면, 다음과 같이 튜플 타입과 나머지 파라미터를 이용하면 된다.

- 함수 매개변수의 타입을 정의할 때 튜플 타입을 이용해 첫번째 요소의 타입은 T, 그리고 나머지 요소의 타입은 …unknown[] 으로 길이도 타입도 상관 없도록 정의한다.
- 함수를 호출하고 [1, “hello”, “mynameis”] 같은 배열 타입의 값을 인수로 전달하면 T는 첫번째 요소의 타입인 Number 타입이 된다. 따라서 함수 반환값 타입또한 Number 타입이 된다.
사례 4.
- 타입 변수를 제한하는 사례이다.
- 타입 변수를 제한한다는 것은 함수를 호출하고 인수로 전달할 수 있는 값의 범위에 제한을 두는 것을 의미한다.
- 다음은 타입 변수를 적어도 length 프로퍼티를 갖는 객체 타입으로 제한한 예시이다.

- 타입 변수를 제한할 때에는
확장(extends)을 이용한다.
- 위와 같이
T extends { length : number } 라고 정의하면 T는 이제 { length : number } 객체 타입의 서브 타입이 된다.
- 바꿔말하면 이제 T는 무조건 Number 타입의 프로퍼티 length 를 가지고 있는 타입이 되어야 한다는 것이다.
- 따라서 이렇게
extends를 이용해 타입 변수를 제한하면 아래와 같은 결과가 나타난다.
- 1번 호출은 인수로 length 프로퍼티가 존재하는 String 타입의 값을 전달했으므로 허용된다.
- 2번 호출은 인수로 length 프로퍼티가 존재하는 Number[] 타입의 값을 전달했으므로 허용된다.
- 3번 호출은 인수로 length 프로퍼티가 존재하는 객체 타입의 값을 전달했으므로 허용된다.
- 4번 호출은 인수로 undefined을 전달했으므로 오류가 발생한다.
- 5번 호출은 인수로 null을 전달했으므로 오류가 발생한다.
map, forEach 메서드 타입 정의하기
Map 메서드 타입 정의하기
- 자바스크립트의 배열 메서드
Map은 다음과 같이 원본 배열의 각 요소에 콜백함수를 수행하고 반환된 값들을 모아 새로운 배열로 만들어 반환한다.

map 메서드를 직접 함수로 만들고 타입도 정의해보자. 먼저 제네릭 함수가 아닌 일반적인 함수로 만든다.
function map(arr: unknown[], callback: (item: unknown) => unknown): unknown[] {}
- 메서드를 적용할 배열을 매개변수 arr로 받고, 콜백 함수를 매개변수 callback으로 받는다.
- map 메서드는 모든 타입의 배열에 적용할 수 있기 때문에 arr의 타입은 unknown[]으로 정의한다. callback의 타입은 배열 요소 하나를 매개변수로 받아 특정 값을 반환하는 함수로 정의한다.
- 함수 타입 표현식을 이용했다. 마지막으로 map 메서드의 반환값의 타입은 배열 타입으로 정의한다.
- 다음으로는 이 함수에 타입 변수를 선언하여 제네릭 함수로 만든다.
function map<T>(arr: T[], callback: (item: T) => T): T[] {}
- 모든 unknown 타입을 타입 변수 T로 대체한다. 다음으로는 함수 내부를 구현한다.

- 매개변수 arr에 number[] 타입의 배열을 제공하니 타입변수 T가 number로 추론되고 그 결과 map 함수의 반환값 타입도 number[]가 되었다.
- 그런데 함수 호출부분을 아래와 같이 한다면 오류가 발생한다.
map(arr, (it) => it.toString()); // ❌
- 콜백함수가 모든 배열 요소를 String 타입으로 변환하도록 수정했다. 이러면 오류가 발생한다.
- 첫번째 인수로 arr을 전달했을때 타입 변수 T에는 number 타입이 할당되었기 때문에 콜백 함수의 반환값 타입도 number 타입이 되어야 하기 때문이다.
- 그런데 map 메서드는 이렇게 원본 배열 타입과 다른 타입의 배열로도 변환할 수 있어야 한다. 따라서 타입 변수를 하나 더 추가해 다음과 같이 수정한다.

- 원본 배열의 타입과 새롭게 반환하는 배열의 타입을 다르게 설정해주니 오류가 없어졌다!
ForEach 메서드 타입 정의하기
forEach 메서드는 다음과 같이 배열의 모든 요소에 콜백함수를 한번씩 수행해주는 메서드이다.

- ForEach 메서드는 Map 메서드보다 훨씬 만들기도 쉽고 타입 정의도 간단하다.

- Map과 동일하게 2개의 매개변수를 받는다.
- 첫번째 매개변수 arr에는 순회 대상 배열을 제공받고, 두번째 매개변수 callback에는 모든 배열 요소에 수행할 함수를 제공 받는다.
- 이때 아까 Map 메서드의 타입 정의와는 달리 forEach 메서드는 반환값이 없는 메서드이므로 콜백 함수의 반환값 타입을
void로 정의한다.
제네릭 인터페이스, 제네릭 타입 별칭
제네릭 인터페이스
- 제네릭은 인터페이스에도 적용할 수 있다. 다음과 같이 인터페이스에 타입 변수를 선언해 사용한다.

- 키페어를 저장하는 객체의 타입을 제네릭 인터페이스로 정의했다.
- 다음과 같이 변수의 타입으로 정의하여 사용할 수도 있다.

- 변수 keyPair의 타입으로
KeyPair<string, number>를 정의했다. 그 결과 K에는 string, V에는 number 타입이 각각 할당되어 key 프로퍼티는 string 타입이고, value 프로퍼티는 number 타입인 객체 타입이 된다. 따라서 값으로 해당 타입의 객체를 저장한다.
- 변수 keyPair2의 타입으로
KeyPair<boolean, string[]>를 정의했다. 그 결과 K에는 boolean, V에는 string[] 타입이 각각 할당되어 key 프로퍼티는 boolean 타입이고, value 프로퍼티는 string[] 타입인 객체 타입이 된다. 따라서 값으로 해당 타입의 객체를 저장한다.
- 이때 주의해야 할 점은 제네릭 인터페이스는 제네릭 함수와는 달리 변수의 타입으로 정의할 때, 반드시 꺽쇠와 함께 타입 변수에 할당할 타입을 명시해주어야 한다.
- 그 이유는 제네릭 함수는 매개변수에 제공되는 값의 타입을 기준으로 타입 변수의 타입을 추론할 수 있지만, 인터페이스는 마땅히 추론할 수 있는 값이 없기 때문이다.
인덱스 시그니쳐와 함께 사용하기
- 제네릭 인터페이스는 인덱스 시그니쳐와 함께 사용하면 다음과 같이 기존보다 훨씬 더 유연한 객체 타입을 정의할 수 있다.

- 한개의 타입 변수 V를 갖는 제네릭 인터페이스 Map을 정의했다.
- 이 인터페이스는 인덱스 시그니쳐로 key의 타입은 string, value의 타입은 V인 모든 객체 타입을 포함하는 타입이다.
- 변수 stringMap의 타입을
Map<string> 으로 정의했다. 따라서 V가 string 타입이 되어 이 변수의 타입은 key는 string이고 value는 string인 모든 프로퍼티를 포함하는 객체 타입으로 정의된다.
- 변수 booleanMap의 타입을
Map<boolean> 으로 정의했다.
- 따라서 V가 boolean 타입이 되어 이 변수의 타입은 key는 string이고, value는 boolean인 모든 프로퍼티를 포함하는 객체 타입으로 정의된다.
제네릭 타입 별칭
- 인터페이스와 마찬가지로 타입 별칭에도 역시 제네릭을 적용할 수 있다.

- 제네릭 타입 별칭을 사용할 때도 제네릭 인터페이스와 마찬가지로 타입으로 정의될 때 반드시 타입 변수에 설정할 타입을 명시해 주어야한다.
제네릭 클래스
- 먼저 제네릭이 아닌 간단한 Number 타입의 리스트를 생성하는 클래스를 하나 만든다.

- list 필드를 private(접근 제어자)으로 설정해 클래스 내부에서만 접근할 수 있도록 만들고, 생성자에서 필드 선언과 함께 초기화한다. 새로운 요소를 추가하는 push, 제거하는 pop, 출력하는 print 메서드도 만들었다.
- 그런데 만약 이때 StringList 클래스도 하나 필요하다면 어떻게 해야 할까? 제네릭 없이는 어쩔 수 없이 새로운 클래스를 하나 더 만들어줘야 한다. 이는 굉장히 비효율적이다.
- 이럴 때 다음과 같이 제네릭 클래스를 사용해 여러 타입의 리스트를 생성할 수 있는 범용적인 클래스를 정의하면된다.

- 클래스의 이름 뒤에 타입 변수를 선언하면 제네릭 클래스가 된다.
- 이 타입 변수는 이제 클래스 내부에서 자유롭게 사용할 수 있다.
- 또 클래스는 생성자를 통해 타입 변수의 타입을 추론할 수 있기 때문에 생성자에 인수로 전달하는 값이 있을 경우 타입 변수에 할당할 타입을 생략해도 된다.
- 만약 타입변수의 타입을 직접 설정하고 싶다면 다음과 같이 하면 된다.

Promise 사용하기
- Promise는 제네릭 클래스로 구현되어 있다. 따라서 새로운 Promise를 생성할 때 다음과 같이 타입 변수에 할당할 타입을 직접 설정해 주면 해당 타입이 바로 resolve 결과값의 타입이 된다.

- reject 함수에 인수로 전달하는 값, 즉 실패의 결과값 타입은 정의할 수 없다. 그냥 unknown 타입으로 고정되어 있기 때문에 catch 메서드에서 사용하려면 타입 좁히기를 사용하는걸 권장한다.
- 만약 어떤 함수가 Promise 객체를 반환한다면 함수의 반환값 타입을 위해 다음과 같이 할 수 있다.

- 또는 더 직관적으로 다음과 같이 반환값 타입을 직접 명시해도 된다. (추천한다!)

이미지 출처: <한 입 크기로 잘라먹는 타입스크립트>
https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8?inst=68d752f7&utm_source=instructor&utm_medium=referral&utm_campaign=inflearn_%ED%8A%B8%EB%9E%98%ED%94%BD_promotion-link