인자들과 반환값에 대하여 형태(타입)에 따라 그에 상응하는 형태(타입)를 가질 수 있다. 제네릭!
타입스크립트에는 2가지 타입이 있다.
즉, 제네릭 타입을 통해 다양한 타입 즉, 다형성을 띨 수 있다.
T라는 제네릭 타입을 선언해 각 배열의 요소 타입에 맞춰서 나오는 모습이다.
type SuperPrint = {
(arr: T[]): T;
};
const superPrint: SuperPrint = (arr) => {
return arr[0];
};
// 유추해서 시그니처를 보여줌
const a = superPrint([1, 2, 3]);
// const superPrint: <number>(arr: number[]) => number;
const b = superPrint([true, false, true]);
// const superPrint: <boolean>(arr: boolean[]) => boolean;
const c = superPrint(["a", "b"]);
const d = superPrint([1, 2, "a", "b", true]);
// const d: string | number | boolean
아래 함수 선언문을 보면 다양한 타입을 받고 있지 않는가? 전 시간에 했던 Call Signature로 다 맞춰줄 수 없는 노릇이다.
여기서 제네릭 타입 T
를 입력해 a
는 (arr: number[])
, d
는 (arr:(number|string|boolean)[])
와 같은 효력을 가지게 된다. 물론 저렇게 모든 조합을 적어줘도 되지만 별로 바람직하진 않다.d
의 호출 시그니처
즉, 제네릭은 타입이 어떻게 나오는지 추론한다는 뜻이다!
우선 제네릭을 쓰는 이유는 any
처럼 처리하기 편하면서도 한 함수 안에서 타입을 담아 전달할 수 있기 때문에 강력한 타입 체크도 겸할 수 있기에 좋다.
제네릭 타입의 변수 이름으로 보통 JS 변수처럼 이름을 붙여줘도 되지만 T(타입), V(밸류)를 많이 쓴다. 앞에 브라켓을 넣어서 변수 선언한다.
// 타입
type SuperPrint = {
<T>(arr: T[]): T;
};
// 인터페이스: 브라켓 有
interface SuperPrint {
<T>(arr: T[]): T;
}
interface SuperPrint {
<TypePlaceholder>(arr: TypePlaceholder[]): TypePlaceholder;
}
// 함수에 바로 적용
const superPrint<T>(arr: T[]): T = (arr) => {
return arr[0];
};
타입의 다형성을 보며 제네릭을 배웠다. 제네릭이 정확히 뭔지 배워보자
제네릭은 C#이나 Java와 같은 정적 언어에서 재사용 가능한 컴포넌트를 만들기 위해 사용하는 기법. 단일 타입이 아닌 다양한 타입에서 작동할 수 있는 컴포넌트를 생성 가능.
또한 ⭐️제네릭은 선언 시점이 아니라 (타입)생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법.
즉, 타입스크립트에서 제네릭을 통해 any
와 달리 타입을 추정하고 인터페이스, 함수 등의 재사용성을 높일 수 있다.
기본적인 사용법은 위에서 봤으니 하나의 함수에서 제네릭을 여러개 써서 활용성을 더욱 높여보자!
interface SuperPrint {
<T, M>(arr: T[], b: M): T;
}
const superPrint: SuperPrint = (arr) => {
return arr[0];
};
const a = superPrint([1, 2, 3], "x");
const b = superPrint([1, 2, 3], [1]);
위의 함수로 T
, M
이라는 제네릭이 들어오고 첫번째 파라미터로 T
(배열)가, 두번째 파라미터로 M
이 들어온다.보면 알겠지만 2번째 파라미터의 타입도 추정해서 받아주는 모습
제네릭은 대부분의 외부 라이브러리들이 쓴다. 위의 이점이 있으니까 말이다. 그렇다면 사용하는 우리는? 제네릭을 알 수가 없는데? 하지만 여기서도 우리만의 제네릭을 갖다가 붙이면 제네릭이 알아서 타입 추정을 해주고 받아오니 우리는 선언했던 제네릭을 쓰면 된다는 것이다!
즉! 외부 라이브러리를 사용할 때 타입을 모른다면?! 제네릭을 무적권 권장한다! 타입스크립트가 타입을 추정하도록 나두는 것이 제일 베스트!
이렇게 타입들을 확장하며 타입을 지정할 수 있다.
// 제네릭 E로 타입 지정이 된다.
type Player<E> = {
name: string;
extraInfo: E;
};
type MePlayer = Player<MeExtra>;
type MeExtra = { age: number };
// MePlayer를 쓴 함수들은 extraInfo가 age:num으로 오는 것들이다.
const player: MePlayer = {
name: "joseph",
extraInfo: {
age: 23,
},
};
// 이렇게 직접 지정 가능
const player2: Player<null> = {
name: "Yee",
extraInfo: null,
};
이런 식으로 활용이 무궁무진하다. Player
를 가져와 직접 지정할 수도 있고,Player
를 활용한 MePlayer
를 가져와 extraInfo
가 객체로 이루어진 애들한테도 사용할 수 있다.
대부분 라이브러리와 개발자들은 제네릭을 쓰며 상속을 받아서 편하게 쓰는 식으로 개발해왔다.
제네릭이 쓰이는 가장 대표적인 것이 리액트 훅인 useState
다. 리액트 + TS를 쓸 때 많이 쓰던 방식이 있다.
// 에러 타입지정 필요
const [] = useState();
// 브라켓으로 감싼 제네릭을 써서 타입 지정
const [] = useState<number>();
이런 식으로 브라켓을 써서 타입을 지정했지 아니한가? 저렇게 Hook들에게도 제네릭이 있어 저렇게만 써줘도 타입지정이 가능했던 것이다! 유레카!@
interface SuperPrint {
<T>(arr: T[]): T;
}
interface SuperPrintPlus {
<T>(arr: T[], item: T): T[];
}
const last = (arr): SuperPrint => {
const arrLen = arr.length - 1;
return arr[arrLen];
};
const prepend = (arr, item): SuperPrintPlus => {
return [item, ...arr];
};
// 이건 안됨..
console.log(last([1,2,3]))
console.log(prepend([1,2,3], 0))
// function last<T>(arr: T[]): T
function last<T>(arr: T[]): T {
return arr[arr.length - 1];
}
// function prepend<T>(arr: T[], item: T): T[]
function prepend<T>(arr: T[], item: T): T[] {
return [item, ...arr];
}
console.log(last([1,2,3]))
console.log(prepend([1,2,3], 0))
아래는 됨 무슨 차이지..? 위에는 파라미터가 any
타입이라 안된다고 뜬다.. 똑같이 제네릭으로 적용했는데 왜 타입을 추정하지 못했을까?