타입스크립트 제네릭

함수, 클래스, 인터페이스 등에서 여러 타입을 동시에 지원하는 기능

사용하는 이유와 장점

1. 재사용성

특정한 타입에 종속되지 않고 다양한 타입을 처리하는 코드를 작성할 수 있다.

2. 타입 안정성

제네릭을 사용하면 함수나 클래스의 인자, 반환 값 등에 대해 타입을 미리 지정할 수 있으므로, 컴파일 시간에 타입 오류를 미리 체크하여 런타임 에러를 방지할 수 있다.

3. 유연한 데이터 구조

컨테이너 클래스나 데이터 구조를 만들 때 제네릭을 사용하면 여러 타입의 데이터를 다룰 수 있어 유연성을 제공한다.

4. 라이브러리 개발

제네릭을 활용하여 라이브러리를 개발할 경우, 사용자들이 다양한 타입을 지원하면서도 타입 안정성을 유지할 수 있다.

5. 컬렉션 데이터 처리

제네릭을 사용하면 배열, 리스트, 스택 등 컬렉션 데이터를 처리하는 함수나 클래스에서 다양한 타입의 데이터를 처리할 수 있다.

사용 케이스

1. 컬렉션 데이터 처리

배열, 리스트와 같은 컬렉션 데이터를 다룰 때, 여러 타입의 요소를 다루어야 하는 경우

// 배열의 첫 번째 요소를 반환하는 함수 (제네릭 사용)
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr.length > 0 ? arr[0] : undefined
}
 
const numbers = [1, 2, 3, 4, 5]
const firstNumber = getFirstElement(numbers) // number 타입으로 추론
 
const names = ['Alice', 'Bob', 'Charlie']
const firstName = getFirstElement(names) // string 타입으로 추론

2. 유틸리티 함수

제네릭을 사용하여 각종 유틸리티 함수를 작성하면, 여러 데이터 타입에서 재사용 가능한 함수를 만들 수 있다.

// 두 개의 값을 바꾸는 함수 (제네릭 사용)
function swap<T>(a: T, b: T): [T, T] {
  return [b, a]
}
 
const x = 10
const y = 20
const [swappedX, swappedY] = swap(x, y) // number 타입으로 추론
 
const str1 = 'Hello'
const str2 = 'World'
const [swappedStr1, swappedStr2] = swap(str1, str2) // string 타입으로 추론

3. 데이터 구조

스택, 큐, 연결 리스트 등과 같은 데이터 구조를 만들 때, 여러 타입의 데이터를 처리하기 위해 사용할 수 있다.

// 스택 구현 (제네릭 사용)
class Stack<T> {
  private data: T[] = []
 
  push(item: T) {
    this.data.push(item)
  }
 
  pop(): T | undefined {
    return this.data.pop()
  }
}
 
const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
numberStack.push(3)
 
const poppedNumber = numberStack.pop() // number 타입으로 추론
 
const stringStack = new Stack<string>()
stringStack.push('one')
stringStack.push('two')
stringStack.push('three')
 
const poppedString = stringStack.pop() // string 타입으로 추론

4. 라이브러리

라이브러리를 개발할 때 사용자가 다양한 데이터 타입을 사용할 수 있도록 하면서도 타입 안정성을 유지하고자 할 때 사용

// 기본적인 배열 유틸리티 라이브러리 (제네릭 사용)
class MyArrayUtils<T> {
  private arr: T[]
 
  constructor(arr: T[]) {
    this.arr = arr
  }
 
  findIndex(item: T): number {
    return this.arr.indexOf(item)
  }
 
  map<U>(callback: (item: T) => U): U[] {
    return this.arr.map(callback)
  }
}
 
const numbers = [1, 2, 3, 4, 5]
const arrayUtils = new MyArrayUtils(numbers)
 
const index = arrayUtils.findIndex(3) // number 타입으로 추론
 
const squaredNumbers = arrayUtils.map((num) => num * num) // number[] 타입으로 추론