정의
기본적으로 함수는 여러가지 다른 모양 또는 다른 형태를 가지고 있음 (여러 parameter를 가질 수 있음)
제네릭!
배열을 받고, 그 배열의 결과를 print 해주는 함수
// 세 개의 call signature가 있음.
// 근데 number, string이 섞인 배열을 받고 싶으면 또 시그니처를 작성해야 함..
type SuperPrint = {
(arr: number[]): void;
(arr: boolean[]): void;
(arr: string[]): void;
// (arr: number|string): void;
}
const superPrint: SuperPrint = (arr) => {
arr.forEach(item => console.log(item);
}
superPrint([1, 2, 3, 4]);
superPrint([true, false, true]);
superPrint(["a", "b", "c"]);
다형성을 활용하는 더 좋은 방법이 있음. 타입스크립트에게 더 나은 방법으로 얘기해줄 수 있음!
어떤 배열이든 프린트가 잘 동작되게 만들기. 모든 가능성을 다 조합해서 만드는 방법은 별로 좋지 않음. call signature를 작성할 때 여기 들어올 확실한 타입을 모를 때 generic을 사용함. (concrete type이 되겠지만 그 타입을 미리 알 수 없을 때)
type SuperPrint = {
// 이 argument가 제네릭을 사용할거다 받는다! 라고 알려주기
// 보통 T, V 알파벳으로 많이 작성함
// => 이 call signature가 제네릭을 받는다라는 걸 알려주는 방법
<TypePlaceholder>(arr: TypePlaceholder[]): void;
}
const superPrint: SuperPrint = (arr) => {
arr.forEach(item => console.log(item);
}
// 모두 문제없이 해결
superPrint([1, 2, 3, 4]); // number 타입의 배열로 동작하구나!
superPrint([true, false, true]);
// 마우스를 올려보면 > const superPrint: <boolean>(arr: boolean[]) => void
// superPrint 함수에 boolean 타입이 주어졌군
superPrint(["a", "b", "c"]);
superPrint([1, 2, true, 'ㅁ']);
// <string|number|boolean>(arr: (string|number|boolean[]) => ...
타입스크립트는 값들을 보고 타입을 유추하고, 기본적으로 그 유추한 타입으로 call signature를 보여줌.
리턴 타입 바꾸기! superPrint는 arr를 받고, 그 배열의 첫 번째 요소를 리턴.
type SuperPrint = {
<T>(arr: T[]): T;
}
const superPrint: SuperPrint = (arr) => arr[0];
const a = superPrint([1, 2, 3, 4]); // : number
const b = superPrint([1, 2, true, 'ㅁ']); // : string | number | boolean
직접 모든 call signature를 작성하는 것은 별로 좋은 생각이 아님. 바로 제네릭을 사용해야 할 때! 제네릭은 기본적으로 placeholder를 사용해 우리가 작성한 코드의 타입 기준으로 바꿔줌
=> 타입스크립트가 우리 코드를 보고 알아냄. 그리고 placeholder를 발견한 타입으로 대체
만약 함수의 목표가 아무거나 넣어도 아무거나 리턴되는 것이라면 any를 넣는 게 나을 거라 생각할 수도 있음. 이건 우릴 더 이상 지켜주지 않음. 타입스크립트로부터 보호받을 수 없음.
https://www.typescriptlang.org/docs/handbook/2/generics.html#hello-world-of-generics
짧게 요약하자면, 'any'를 사용하는 것은 어떤 타입이든 받을 수 있다는 점에서 'generics'과 같지만 함수를 반환하는데 있어 'any'는 받았던 인수들의 타입을 활용하지 못한다. 즉, 'generics'은 어떤 타입이든 받을 수 있다는 점에서 'any'와 같지만 해당 정보를 잃지 않고 타입에 대한 정보를 다른 쪽으로 전달할 수 있다는 점이 다르다
superPrint타입 제네릭을 하나 더 추가하고 싶다면?
제일 먼저 해야할 일은 제네릭을 사용할거라고 얘기하고 이름을 작성하고, 이 제네릭을 어디에서 사용할 건지 얘기하기
type SuperPrint = <T, V>(a: T[], b: V): T
라이브러리를 만들거나, 다른 개발자가 사용할 기능을 개발하는 경우엔 제네릭이 유용할 거임. 그 외 대부분의 경우엔 직접 작성할 일은 없음
다른 방법으로 제네릭 선언 - 일반 함수로 대체
function superPrint<V>(a: V[]) {
return a[0];
}
const a = superPrint<boolean>([1, 2, 3, 4]); // 빨간줄 에러
// 타입스크립트에 boolean이라고 명시해주고 있는 것. 꼭말해줄필요X
// => 항상 타입스크립트가 스스로 타입을 유추하도록 하는 것이 좋음
// 타입스크립트는 똑똑함!
제네릭을 사용하는 또다른 경우
type Player<E> = {
name: string
extreInfo: E
}
const nico: Player<{ favFood: string }> = {
name: 'nico',
extraInfo: {
favFood: 'kimchi;
}
}
// =
type NicoPlayer = Player<{ favFood: string }>
const nico: NicoPlayer = {
name: 'nico',
extraInfo: {
favFood: 'kimchi;
}
}
// 원하는대로 코드를 확장
// 타입을 생성하고 그 타입을 또다른 타입에 넣어서 사용
type NicoExtra = { favFood: string }
type NicoPlayer = Player<NicoExtra>
많은 것들이 있는 큰 타입을 하나 가지고 있는데, 그 중 하나가 달라질 수 있는 타입이라면 제네릭을 넣기. 그럼 많은 재사용이 가능.
또 제네릭은 함수에서만 쓰이는 게 아니라 정말정말 많은 곳서 쓰임. 예를 들면, 대부분의 기본적인 타입스크립트의 타입은 제네릭으로 만들어져 있음.
Array를 생성하는데 제네릭을 받고 있음.
type A = Array<number>
let a: A = [1, 2, 3, 4]
number[] === Array<number>