[Typescript] Generic

이한슬·2024년 1월 15일

Typescript

목록 보기
4/7

Generic

call signature를 정의할 때 어떤 타입이 들어오게 될지 확실하지 않다면 generic을 사용할 수 있다. 또는, 변수나 함수의 타입을 미리 정하지 않고 사용하고 싶을 때도 사용할 수 있다.
generic은 일반적으로 <>안에 타입명을 적어 변수나 함수 이름 뒤에 붙여서 사용하게 된다.

type A<T> = Array<T>;

let a: A<number> = [1, 2];

function printArray<T>(arr: Array<T>) {
  console.log(arr);
}

printArray(a);

다음과 같이 어떤 타입이든지 인자로 받아오면 그대로 하나씩 출력해주는 printArray 함수가 있다고 생각해보자. 이처럼 모든 타입에 대하여 call signature를 일일이 정의해야한다면 매우 불편할 것이다. 마지막에 있는 printArray([1, 'b', false])의 call signature처럼 모든 경우를 생각하여 call signature를 만들어주어야 한다.

type PrintArray = {
  (arr: number[]):void
  (arr: string[]):void
  (arr: boolean[]):void
  (arr: (number|string|boolean)[]):void
}

const printArray: PrintArray = (arr) => {
  arr.forEach(i => console.log(i))
}

printArray([1, 2, 3, 4])
printArray(['a', 'b', 'c'])
printArray([true, false])
printArray([1, 'b', false])

call signature에서 generic을 사용하려면 call signature의 앞에 <타입명>을 붙여주면 된다. 타입의 이름은 대문자로 시작하기만하면 원하는대로 지을 수 있다. T, V처럼 한 글자 대문자들이 많이 사용되곤 한다. generic을 활용하여 코드를 바꿔보면 이렇게 된다.

type PrintArray = {
  <T>(arr: T[]):void
}

const printArray: PrintArray = (arr) => {
  arr.forEach(i => console.log(i))
}

printArray([1, 2, 3, 4])
printArray(['a', 'b', 'c'])
printArray([true, false])
printArray([1, 'b', false])

printArray 함수의 사용 시점에 call signature가 결정이 되며, 각각에 맞는 타입을 인자를 통해 추론하여 call signature가 결정된다. 즉, printArray([1, 2, 3, 4])에서는 Tnumber가 될 것이고, printArray(['a', 'b', 'c'])에서는 Tstring이 되는 것이다.

만약 이번에는 배열의 요소들을 모두 출력하는 것이 아니라 배열의 첫번째 요소만 리턴하는 함수로 바꾸려면 리턴 타입은 어떻게 정의할까? 이때는 간편하게 리턴타입을 T로 주면 된다.

type ReturnArray = <T>(arr: T[]) => T

const returnArray: ReturnArray = (arr) => arr[0]

const a = returnArray([1, 2, 3, 4])
const b = returnArray(['a', 'b', 'c'])
const c = returnArray([true, false])
const d = returnArray([1, 'b', false])

a의 타입은 number, b의 타입은 string, c의 타입은 boolean, d의 타입은 string | number | boolean으로 자동으로 추론된다.

그렇다면 generic을 사용하는 것이 any 타입을 사용하는 것과 어떤 점이 다를까? any를 사용하여 코드를 작성하면 다음과 같이 되며, 코드를 실행하기 전에는 오류가 발생하지 않는다.

type ReturnArray = (arr: any[]) => any

const returnArray: ReturnArray = (arr) => arr[0]

const a = returnArray([1, 2, 3, 4])
const b = returnArray(['a', 'b', 'c'])
const c = returnArray([true, false])
const d = returnArray([1, 'b', false])

console.log(d.toUpperCase())

그러나 이 경우, a, b, c, d 모두 any 타입으로 선언되며, 코드를 실행하고 나서야 d.toUpperCase()에서 에러를 찾을 수 있다. generic을 사용한다면 사용하는 함수의 call signature를 추론하여 만들어주기 때문에 string 이외의 여러 타입이 섞여 있을 수 있는 d에 toUpperCase()를 사용할 수 없다는 것을 코드를 실행하기 전에 오류를 알려준다.

generic을 여러 개 사용하려면 <>안에 여러 개를 정의해주면 된다. 타입스크립트는 generic을 처음 인식했을 때와 generic의 순서를 기반으로 generic의 타입을 추론하므로 순서에 맞추어 인자를 넣어주면 된다.

type ReturnArray = <T, V>(arr: T[], value: V) => T

const returnArray: ReturnArray = (arr, value) => arr[0]

const a = returnArray([1, 2, 3, 4], 'first')
const b = returnArray(['a', 'b', 'c'], 'second')
const c = returnArray([true, false], 3)
const d = returnArray([1, 'b', false], 4)

call signature를 미리 만들고 화살표 함수를 사용하는 방법이 아닌 function을 이용해서 만들면 이렇게 된다.

function returnArray<T, V>(arr: T[], value: V) : T {
  return arr[0]
}

const a = returnArray([1, 2, 3, 4], 'first')
const b = returnArray(['a', 'b', 'c'], 'second')
const c = returnArray([true, false], 3)
const d = returnArray([1, 'b', false], 4)

generic은 함수를 사용할 때 말고도 유용하게 사용될 수 있다. 다음처럼 원하는대로 코드를 계속 확장할 수 있다.

type Cafe<Menu> = {
  name: string
  menu: Menu
}

type MenuList = {
  coffee?: string
  cake?: string
}

type CafeMaker = Cafe<MenuList>

const starbucks: CafeMaker = {
  name: 'starbucks',
  menu: {
    coffee: 'Dolce Latte'
  }
}

const twosome: Cafe<null> = {
  name: 'twosome',
  menu: null
}

CafeMenu라는 generic을 가지고 있으며, Cafe 타입의 변수를 만들 때 직접 만든 타입인 MenuList 타입을 generic으로 전달하고 있다.
starbucks의 경우, genericMenuMenuList 타입으로 전달되었고, twosome의 경우, Menunull로 전달되었다.
이렇게 generic을 활용하여 재사용을 여러번 할 수 있는 코드를 작성할 수 있다.

profile
궁금하면 일단 먹어보는 소프트웨어 전공생

0개의 댓글