제네릭이란
함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동작하도록 만들어 주는 타입스크립트의 놀라운 기능 중 하나
1. 제네릭이 필요한 상황
1) 제네릭 사용 전
- 다양한 타입의 매개변수를 받고 해당 매개변수를 그대로 반환하는 함수가 하나 필요하다고 가정
- 다양한 타입의 매개변수를 제공받아야 하기 때문에 매개변수 value의 타입을 일단 any 타입으로 입력
- func 함수에 number 또는 string 타입을 인수로 전달해도 any타입으로 추론하는 문제점 발생
// 제네릭 사용 전
function func(value: any) {
return value;
}
let num = func(10);
// any 타입
let str = func("string");
// any 타입
❗️원하는 것
인수로 Number 타입의 값을 전달하면 반환 타입이 Number가 되고, 인수로 String 타입의 값을 전달하면 반환값의 타입도 String 타입이 되는 것
→ 제네릭 사용
2. 제네릭(Generic) 함수
두루두루 모든 타입의 값을 다 적용할 수 있는 그런 범용적인 함수
function func<T>(value: T): T {
return value;
}
let num = func(10);
// number 타입
- 함수 이름 뒤에 꺽쇠를 열고 타입을 담는 변수인 타입 변수 T를 선언
- 그리고 매개변수와 반환값의 타입을 이 타입변수 T로 설정
- T에 어떤 타입이 할당될 지는 함수가 호출될 때 결정된다.
- func(10) 처럼 Number 타입의 값을 인수로 전달하면 매개변수 value에 Number 타입의 값이 저장되면서 T가 Number 타입으로 추론
- 이때 T가 Number 타입으로 추론되고, 이때의 func 함수의 반환값 타입또한 Number 타입이 된다.
- 제네릭 함수를 호출할 때 다음과 같이 타입 변수에 할당할 타입을 직접 명시하는 것도 가능
function func<T>(value: T): T {
return value;
}
let arr = func<[number, number, number]>([1, 2, 3]);
T에 [Number, Number, Number] 튜플 타입이 할당됨
- 매개변수 value와 반환값 타입이 모두 튜플 타입이 됨
- 만약 위 코드에서 타입 변수에 할당할 타입을 튜플 타입으로 설정하지 않았다면 T가 number[] 타입으로 추론
- 타입스크립트는 타입을 추론할 때 항상 일반적이고 좀 더 범용적인 타입으로 추론하기 때문
- 타입 변수에 할당하고 싶은 특정 타입이 존재한다면 함수 호출과 함께 꺽쇠를 열고 직접 명시해주는게 좋다.
2. 제네릭 예시
1) 사례 1
- 만약 2개의 타입 변수가 필요한 상황이라면 다음과 같이 T, U 처럼 2개의 타입 변수를 사용
function swap<T, U>(a: T, b: U) {
return [b, a];
}
const [a, b] = swap("1", 2);
- 위 코드에서 T는 String 타입으로 U는 Number 타입으로 추론
2) 사례 2
- 다양한 배열 타입을 인수로 받는 제네릭 함수를 만들어야 할 때
function returnFirstValue<T>(data: T[]) {
return data[0];
}
let num = returnFirstValue([0, 1, 2]);
// number
let str = returnFirstValue([1, "hello", "mynameis"]);
// number | string
- 함수 매개변수 data의 타입을 T[]로 설정했기 때문에 배열이 아닌 값은 인수로 전달할 수 없다.
- 배열을 인수로 전달하면 T는 배열의 요소 타입으로 할당
- 첫번째 호출에서는 인수로 Number[] 타입의 값을 전달했으므로 이때의 T는 Number 타입으로 추론
- 이때의 함수 반환값 타입은 Number 타입
- 두번째 호출에서는 인수로 (String | Number)[] 타입의 값을 전달했으므로 이때의 T는 String | Number 타입으로 추론
- 이때의 함수 반환값 타입은 String | Number 타입
3) 사례 3
- 사례2에서 만약 반환값의 타입을 배열의 첫번째 요소의 타입이 되도록 하려면 다음과 같이 튜플 타입과 나머지 파라미터를 이용
function returnFirstValue<T>(data: [T, ...unknown[]]) {
return data[0];
}
let str = returnFirstValue([1, "hello", "mynameis"]);
// number
- 함수 매개변수의 타입을 정의할 때 튜플 타입을 이용해 첫번째 요소의 타입은 T 그리고 나머지 요소의 타입은 …unknown[] 으로 길이도 타입도 상관 없도록 정의
- 함수를 호출하고 [1, “hello”, “mynameis”] 같은 배열 타입의 값을 인수로 전달하면 T는 첫번째 요소의 타입인 Number 타입이 된다.
- 따라서 함수 반환값 타입또한 Number 타입이다.
4) 사례 4
- 타입 변수를 제한하는 사례
- 타입 변수를 제한한다는 것은 함수를 호출하고 인수로 전달할 수 있는 값의 범위에 제한을 두는 것을 의미한다.
- 타입 변수를 적어도 length 프로퍼티를 갖는 객체 타입으로 제한한 예시
function getLength<T extends { length: number }>(data: T) {
return data.length;
}
getLength("123"); // ✅
getLength([1, 2, 3]); // ✅
getLength({ length: 1 }); // ✅
getLength(undefined); // ❌
getLength(null); // ❌
- 타입 변수를 제한할 때에는 확장(extends)을 이용
- 위와 같이 T extends { length : number } 라고 정의하면 T는 이제 { length : number } 객체 타입의 서브 타입이 된다.
- 바꿔말하면 이제 T는 무조건 Number 타입의 프로퍼티 length 를 가지고 있는 타입이 되어야 한다는 것
- 1번 호출은 인수로 length 프로퍼티가 존재하는 String 타입의 값을 전달 했으므로 허용
- 2번 호출은 인수로 length 프로퍼티가 존재하는 Number[] 타입의 값을 전달 했으므로 허용
- 3번 호출은 인수로 length 프로퍼티가 존재하는 객체 타입의 값을 전달 했으므로 허용
- 4번 호출은 인수로 undefined을 전달했으므로 오류 발생
- 5번 호출은 인수로 null을 전달했으므로 오류 발생
Map 메서드 타입 정의하기
💡
자바스크립트의 배열 메서드 Map
다음과 같이 원본 배열의 각 요소에 콜백함수를 수행하고 반환된 값들을 모아 새로운 배열로 만들어 반환한다.
const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2);
// [2, 4, 6]
1. map 메서드를 직접 함수로 만들고 타입도 정의하기
1) 먼저 제네릭 함수가 아닌 일반적인 함수로 만든다.
function map(arr: unknown[], callback: (item: unknown) => unknown): unknown[] {}
- 메서드를 적용할 배열을 매개변수 arr로 받고, 콜백 함수를 매개변수 callback으로 받는다.
- map 메서드는 모든 타입의 배열에 적용할 수 있기 때문에 arr의 타입은 unknown[]으로 정의
- callback의 타입은 배열 요소 하나를 매개변수로 받아 특정 값을 반환하는 함수로 정의한다.
- 함수 타입 표현식을 이용
- 마지막으로 map 메서드의 반환값의 타입은 배열 타입으로 정의
2) 이 함수에 타입 변수를 선언하여 제네릭 함수로 만든다.
function map<T>(arr: T[], callback: (item: T) => T): T[] {}
- 모든 unknown 타입을 타입 변수 T로 대체
3) 다음으로는 함수 내부를 구현한다.
function map<T>(arr: T[], callback: (item: T) => T): T[] {
let result = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i]));
}
return result;
}
- map 함수 선언, 타입 정의
4) 함수를 호출한다.
const arr = [1, 2, 3];
function map<T>(arr: T[], callback: (item: T) => T): T[] {
(...)
}
map(arr, (it) => it * 2);
// number[] 타입의 배열을 반환
// 결과 : [2, 4, 6]
- 매개변수 arr에 number[] 타입의 배열을 제공하니 타입변수 T가 number로 추론되고 그 결과 map 함수의 반환값 타입도 number[]가 되었다.
❗️하지만 함수 호출을 다음과 같이 수정하면 오류가 발생
const arr = [1, 2, 3];
function map<T>(arr: T[], callback: (item: T) => T): T[] {
(...)
}
map(arr, (it) => it.toString()); // ❌
- 콜백함수가 모든 배열 요소를 String 타입으로 변환하도록 수정하면 오류가 발생
- 첫번째 인수로 arr을 전달했을 때 타입 변수 T에는 number 타입이 할당되었기 때문에 콜백 함수의 반환값 타입도 number 타입이 되어야 하기 때문
- 그런데 map 메서드는 이렇게 원본 배열 타입과 다른 타입의 배열로도 변환할 수 있어야 한다.
💡 따라서 타입 변수를 하나 더 추가해 다음과 같이 수정
const arr = [1, 2, 3];
function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
(...)
}
map(arr, (it) => it.toString());
// string[] 타입의 배열을 반환
// 결과 : ["1", "2", "3"]
- 원본 배열의 타입과 새롭게 반환하는 배열의 타입을 다르게 설정
2. ForEach 메서드 타입 정의하기
💡
forEach 메서드
다음과 같이 배열의 모든 요소에 콜백함수를 한번씩 수행해주는 메서드
const arr2 = [1, 2, 3];
arr2.forEach((it) => console.log(it));
// 출력 : 1, 2, 3
1) ForEach 메서드 타입 정의
function forEach<T>(arr: T[], callback: (item: T) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i]);
}
}
- Map과 동일하게 2개의 매개변수를 받는다.
- 첫번째 매개변수 arr에는 순회 대상 배열을 제공받고 두번째 매개변수 callback에는 모든 배열 요소에 수행할 함수를 제공 받는다.
- 이때 Map 메서드의 타입 정의와는 달리 forEach 메서드는 반환값이 없는 메서드이므로 콜백 함수의 반환값 타입을 void로 정의
'Frontend > TypeScript' 카테고리의 다른 글
[TypeScript] 7. 타입 조작하기 (0) | 2025.01.29 |
---|---|
[TypeScript] 6-2. 제네릭 활용 (1) | 2025.01.28 |
[TypeScript] 5. 클래스 (0) | 2025.01.27 |
[TypeScript] 4. 인터페이스 (0) | 2025.01.26 |
[TypeScript] 3. 함수와 타입 (0) | 2025.01.25 |