Frontend/TypeScript

[TypeScript] 6-1. 제네릭이란?

ayeongjin 2025. 1. 28. 03:03

제네릭이란

함수나 인터페이스, 타입 별칭, 클래스 등을 다양한 타입과 함께 동작하도록 만들어 주는 타입스크립트의 놀라운 기능 중 하나

 

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] 튜플 타입이 할당됨

  1. 매개변수 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]);
  }
}
  1. 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