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])에서는 T가 number가 될 것이고, printArray(['a', 'b', 'c'])에서는 T가 string이 되는 것이다.
만약 이번에는 배열의 요소들을 모두 출력하는 것이 아니라 배열의 첫번째 요소만 리턴하는 함수로 바꾸려면 리턴 타입은 어떻게 정의할까? 이때는 간편하게 리턴타입을 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
}
Cafe는 Menu라는 generic을 가지고 있으며, Cafe 타입의 변수를 만들 때 직접 만든 타입인 MenuList 타입을 generic으로 전달하고 있다.
starbucks의 경우, generic인 Menu가 MenuList 타입으로 전달되었고, twosome의 경우, Menu가 null로 전달되었다.
이렇게 generic을 활용하여 재사용을 여러번 할 수 있는 코드를 작성할 수 있다.