🔍 글을 시작하기 전에
해당 글은 타입스크립트 프로그래밍 책과 유데미 타입스크립트 강의를 공부하여 정리한 글입니다.
수정해야 할 부분이 있다면 언제든지 알려주세요!
제네릭(Generic)
지금까지 boolean, string, number
등에 대해 알아보았었는데, 이러한 타입을 구체 타입이라고 한다.
구체 타입은 변수에 전달 될 타입을 정확하게 알고 있으며 실제로 이 타입이 전달되었는지 확인할 때 사용된다.
그러면 이와 반대로 어떤 타입이 사용될지 미리 알 수 없을 때 는 어떻게 해야 할까?
바로 제네릭(Generic)을 이용해서 해결할 수 있다.
함수를 정의하기 위해 타입을 정의해야 한다고 하자.
매개변수의 타입을 정의할 때 지금은 어떤 타입이 올지 알 수 없고, 누군가 함수를 호출 또는 정의할 때 TS가 전달된 매개변수를 통해
타입을 알아서 추론하도록 하는 것이 제네릭이다.
다음은 제네릭을 사용하지 않고 filter
함수를 정의한 코드이다.
type Filter = {
(array: number[], f: (item: number) => boolean) => number[]
(array: string[], f: (item: string) => boolean) => string[]
}
function filter(array, f) {
let result = [];
for (let i = 0; i<array.length; i++) {
let item = array[i];
if (f(item)) {
result.push(item);
}
}
return result;
}
const answer = filter([1,2,3,4], _ => _ < 3);
console.log(answer); // [1, 2]
type을 오버로드 하여 정의하였는데, 이렇게 코드를 작성한다면 더 많은 타입이 입력되는 경우
직접 하나하나 추가해야 하며 코드가 길어질 위험이 있다.
제네릭을 사용하여 다음과 같이 수정할 수 있다.
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
const filter: Filter = (array, f) => {
let result = [];
for (let i = 0; i<array.length; i++) {
let item = array[i];
if (f(item)) {
result.push(item);
}
}
return result;
}
const answer = filter([1,2,3,4], _ => _ < 3);
const answer2 = filter(['a', 'b', 'c'], _ => _ === 'b');
console.log(answer); // [1, 2]
console.log(answer2); // ['b']
타입스크립트는 전달된 array
의 타입을 확인한 뒤 T의 타입을 추론한다.
T의 타입이 추론되면 정의된 모든 T를 추론한 타입으로 대입하게 된다.
TS는 이러한 T를 타입을 매개변수화 한다고 하여 제네릭 타입 매개변수라고 부르고 있다.
(제네릭 타입 매개변수 = 제네릭 타입, 다형성 타입 매개변수, 제네릭)
꺽쇠괄호(<>
)를 사용하여 제네릭 타입 매개변수라는 것을 선언한다.
책에서는 제네릭을 사용함으로써 코드를 일반화하고, 재사용성을 높이고, 간결하게 유지하는 데 도움을 주기 때문에
가능하면 제네릭을 사용할 것을 권장하고 있다.
헷갈렸던 점
책에서는 위 예시를 설명하며 <T>
를 호출 시그니처의 일부로 선언했으므로
TS는 Filter
타입의 함수를 실제 호출할 때 구체 타입을 T로 한정한다고 알려주었다.
그리고 이와 다르게 T의 범위를 Filter
의 타입 별칭으로 한정하려면
Filter
를 사용할 때 타입을 명시적으로 한정하게 해야 한다고 하며 아래 코드를 보여주었다.
type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
const filter: Filter<number> = (array, f) => {
let result = [];
for (let i = 0; i<array.length; i++) {
let item = array[i];
if (f(item)) {
result.push(item);
}
}
return result;
}
나는 이 두 개의 코드의 차이점이 명확하게 와닿지 않아서
"T의 범위를 Filter의 타입 별칭으로 한정하려면 Filter를 사용할 때 타입을 명시적으로 한정하게 해야 한다"라는
설명을 이해하는데 어려움을 겪었다.
chatGPT에도 물어보고 책을 여러 번 읽은 결과 이해를 했고, 정리해보려고 한다.
일단 첫 번째 코드와 두 번째 코드의 큰 차이점은 함수 타입 선언 방식이다.
첫번째 코드는 제네릭 타입 매개변수인 <T>
를 객체 타입에 포함시켰고, 두번째 코드는 함수 타입에 포함시켰다.
// 객체 타입에 포함
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
// 함수 타입에 포함
type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
객체 타입에 포함시키는 경우엔 타입 변수 T를 사용하여 어떤 타입이 와도 타입스크립트가 추론을 하여
다양한 타입을 사용할 수 있다는 것을 의미한다.
반면에 함수 타입에 포함시키는 경우엔 Filter
를 사용하여 함수를 정의할 시
타입 변수 T에 명시적으로 타입을 전달하여 특정 타입에 대해서만 함수를 실행시킬 수 있다는 것을 의미한다.
즉, Filter<number>
는 특정 타입에 대해서만 적용 가능하며, Filter
는 모든 타입에 적용 가능하다.
그래서 첫 번째 코드의 경우 Filter
타입만 전달해도 TS가 알아서 타입을 추론하여 실행시켜 주는데,
두 번째 코드는 무조건 특정 타입을 명시적으로 전달해야 실행이 가능하다.
아래 코드 블록은 타입이 정해지는 과정이다.
// 첫번째 코드
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[]
}
const filter: Filter = (array, f) => {...} // 전달 되는 array의 타입에 따라 T의 타입을 추론
filter([1,2,3,4], _ => _ < 3); // [1, 2] -> array는 number[]이므로 T는 number가 됨
// 두번째 코드
type Filter<T> = {
(array: T[], f: (item: T) => boolean): T[]
}
let filter: Filter<number> = (array, f) => {...} // 명시적으로 전달한 number 타입이 T의 타입이 됨
let filter: Filter = (array, f) => {...} // Error: 제네릭 타입 'Filter'는 한 개의 타입 인수를 요구함
결론은, 첫 번째 코드는 함수를 호출할 때 T를 구체 타입으로 한정하여 각각의 filter
호출에 따라 각각의 T 한정 값을 가지며,
두 번째 코드는 Filter
타입의 함수를 선언할 때 T를 한정한다.
첫 번째 코드를 type 별칭이 아닌, 함수를 정의하면서 제네릭을 사용한다면 다음과 같다.
function filter<T>(array: T[], f: (item: T) => boolean): T[] {...}
map() 제네릭으로 구현해 보기
function map(array: unknown[], f: (item: unknown) => unknown): unknown[] {
let result = [];
for (let i = 0; i<array.length; i++) {
result[i] = f(array[i]);
}
return result
}
책에서 위 코드를 제네릭을 사용한 코드로 변경해 보라고 미니 퀴즈를 내줬고, 나는 이렇게 작성했다.
function map<T>(array: T[], f: (item: T) => T): T[] {
let result = [];
for (let i = 0; i<array.length; i++) {
result[i] = f(array[i]);
}
return result
}
테스트하기 위해 map([1,2,3], (item) => item * 2);
코드를 실행시켜 보았고,
정상적으로 값이 출력되어 정답이라고 생각했다.
하지만 정답을 보니 다음과 같이 두 개의 제네릭 매개변수를 사용한 것을 확인했다.
function map<T,U>(array: T[], f: (item: T) => U): U[] {
let result = [];
for (let i = 0; i<array.length; i++) {
result[i] = f(array[i]);
}
return result
}
제네릭 매개변수가 왜 두 개나 필요한지 이해가 안 됐는데, 이것 저것 테스트 출력을 해보다가
map([1,2,3], (_) => _ + '2');
이 코드를 실행시켜 보니 왜 두 개가 필요한지 확실히 알게 되었다.
map 메서드는 반환 값의 타입이 입력 값의 타입과 달라질 수 있다는 점을 간과하고 있었다.
내가 작성한 코드로 실행시키면 'string' 유형은 'number' 유형에 할당할 수 없습니다.
오류가 발생하는 것을 확인했다.
제네릭을 사용할 때 입력값과 반환값을 정확히 아는 것이 중요하다는 것을 알게 되었다.
'Front-End > TypeScript' 카테고리의 다른 글
타입스크립트[TypeScript] | 4장 연습 문제 (0) | 2023.04.24 |
---|---|
타입스크립트[TypeScript] | 제네릭(Generic) 제약조건 설정하기 (0) | 2023.04.24 |
타입스크립트[TypeScript] | 클래스 상속(extend), 추상 클래스(abstrct), 정적 메서드(static) (1) | 2023.04.18 |
타입스크립트[TypeScript] | 함수 Function 타입 정의하기 (0) | 2023.04.07 |