type SuperPrint = {
(arr: number[]): void // 아무것도 return하지 않음
}
const superPrint: SuperPrint = (arr) => {
arr.forEach(i => console.log(i))
}
number 배열을 받아서 배열의 요소를 각각의 콘솔로 찍는 함수
이때, boolean 타입의 배열이든, string 타입의 배열이든, 모든 배열을 인자로 받고 싶다면?
type SuperPrint = {
(arr: number[]): void // 아무것도 return하지 않음
(arr: boolean[]): void
(arr: string[]): void
}
...
superPrint([1, 2, 3, 4])
superPrint([true, false, true])
superPrint(["a", "b", "c"])
이렇게 매번 call signature를 만들어도 되지만, 다형성을 활용한 더 좋은 방법이 있다.
⇒ ✨ generic type !
타입의 placeholder 같은 개념으로, concrete 타입 대신 사용!
제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.
concrete type ? ex. number, boolean, string, unknown ...
실제 상황에서는 직접 generic을 사용하여 call signature를 생성하는 상황보다는 generic을 사용하는 경우가 더 많다. (라이브러리들이 generic을 통해 생성되기 때문에)
superPrint([1, 2, true, false]) // Err!
위 코드에서 에러가 발생하는 이유는 해당 인자들에 대한 call signature가 없기 때문이다.
type SuperPrint = {
(arr: number[]): void
(arr: boolean[]): void
(arr: string[]): void
(arr: (number|boolean)[]): void
}
이렇게 매번 call signature를 추가한다면, call signature를 모든 가능성을 다 조합해서 만들어야 한다.
어떤 타입의 배열이 들어오더라도, 작동되게 하려면 generic을 사용하면 된다!
TypeScript에게 generic을 사용하고 싶다고 알려야 한다.
⇒ <>
사용하여 정의!
type SuperPrint = {
<T>(arr: T[]): void
}
해당 call signature가 argument로 generic 타입을 받음을 의미하는 코드
<>
안의 이름은 자유지만, 보통 많은 라이브러리, 패키지 등에서 제네릭을 <T>
, <V>
로 표기한다.
// const superPrint: <boolean>(arr: boolean[]) => void
const superPrint: SuperPrint = (arr) => {
arr.forEach(i => console.log(i))
}
superPrint([1, 2, 3, 4])
superPrint([true, false, true])
superPrint(["a", "b", "c"])
superPrint([1, 2, true, false])
✨ generic 타입의 call signature를 만들 경우,
TypeScript가 코드를 분석하여 타입을 유추하고, 유추한 타입으로 call signature를 대체해준다.
⇒ 모든 타입 케이스들을 일일이 작성하지 않아도 된다!
type SuperPrint = {
<T>(arr: T[]): T
}
const superPrint: SuperPrint = (arr) => arr[0]
superPrint
가 무언가를 return하는 함수가 되었으므로, void
(아무것도 return X)를 T
로 변경하기만 하면 된다.
const a = superPrint([1, 2, 3, 4]) // const a: number
const b = superPrint([true, false, true]) // const b: boolean
const c = superPrint(["a", "b", "c"]) // const c: string
const d = superPrint([1, 2, true, false]) // const d: string | number | boolean
마찬가지로, TypeScript가 자동으로 return 타입을 유추한다!
type SuperPrint = (a: any[]): any
const superPrint: SuperPrint = (a) => a[0]
const a = superPrint([1, 2, 3, 4])
a.toUpperCase() // 에러 없음
any를 사용해도 타입 관련 에러는 나지 않는다.
그러나! any를 사용할 경우 타입 검사를 하지 않기 때문에, TypeScript의 보호장치에서 벗어나게 된다.
예시로 a.toUpperCase()
를 실행할 경우, number 타입에 toUpperCase()
를 하는 데도 어떠한 에러도 발생하지 않게 된다.
✨ any는 말 그대로 모든 타입을 허용하는 타입이며, generic은 코드를 분석하여 알맞는 call signature를 생성하는 타입이다.
type SuperPrint = <T, M>(a: T[], b:M) => T
<>
안에 generic명을 추가한 후, generic을 사용하고자 하는 곳에 타입 표시를 해준다.
// 이전 예시에서 사용한 방식 (타입을 따로 선언)
type SuperPrint = {
<T>(arr: T[]): T
}
const superPrint: SuperPrint = (arr) => arr[0]
// 또 다른 방식
function superPrint<T>(a: T[]){
return a[0]
}
위와 같이 작성하여도 동일하게 작동하게 된다.
function superPrint<T>(a: T[]){
return a[0]
}
const a = superPrint<boolean>([1, 2, 3, 4]) // Err!
선언한 함수를 호출할 때에는 <>
로 타입을 명시할 수는 있으나, TypeScript가 타입을 추론하도록 하는 것이 가장 좋으므로 따로 타입을 작성하지 않는 것이 좋다!
현재 위 코드에서 에러가 나는 이유는 TypeScript에 overwrite(덮어쓰기)를 했기 때문이다. (인자의 generic이 boolean 타입인데, number 타입을 인자로 넘겨주었기 때문에)
type Player<E> = {
name: string
extraInfo: E
}
// 위 타입으로 함수를 구현할 때에는 Player의 extraInfo의 키와 타입을 필수적으로 작성해야 한다.
const nico: Player<{favFood: string}> = {
name: "nico",
extraInfo: {
favFood: "kimchi"
}
}
위 코드는 generic을 이용하여 아래와 같은 방식으로 확장이 가능하다.
// generic을 사용해 만들어진 타입, Player
type Player<E> = {
name: string
extraInfo: E
}
type NicoExtra = {
favFood: string
}
// NicoExtra 타입을 Player 타입의 generic에 넣어 사용
type NicoPlayer = Player<NicoExtra>
const nico: NicoPlayer = {
name: "nico",
extraInfo: {
favFood: "kimchi"
}
}
이처럼 타입을 생성하고, 그 타입을 또 다른 타입에 넣어서 사용할 수 있다.
const lynn: Player<null> = {
name: "lynn"
}
extraInfo
값을 가지고 있지 않은 lynn
이라는 변수를 선언
필수적이지 않은 extraInfo
의 타입을 generic으로 사용하였기 때문에, 타입을 새로 만들지 않고 재사용할 수 있다.
✨ 이처럼 많은 요소들을 가지고 있는 하나의 타입이 있고, 그 안의 일부 요소 타입이 달라질 수 있다면 generic을 사용하여 타입 재사용성을 높일 수 있다!
generic은 앞선 예시들처럼 함수에서만 쓰이는 것이 아니며,
대부분 기본적인 타입스크립트의 타입은 generic으로 만들어져 있다.
function printAllNumbers(arr: number[]) {}
function printAllNumbers(arr: Array<number>) {}
위 코드와 아래 코드(generic 형식으로 작성)는 동일하게 작동하는 코드!
useState()
위와 같이 useState
를 사용할 경우, TypeScript는 state
의 타입을 알 수가 없다.
TypeScript에게 state
의 타입을 알려주기 위해서는 아래와 같이 작성해야 한다.
useState<number>()
React.js의 함수인 useState
를 사용할 때 generic을 사용하면, useState
의 call signature가 해당 타입으로 생성된다.