타입스크립트에서는 함수의 파라미터나 리턴값을 함수를 선언할 때 미리 지정해줬어야 했다.
이번엔 타입을 호출하는 순간 지정해 줄 수 있는 제네릭에 대해 배워보자.
제네릭이란 타입을 마치 함수의 파라미터처럼 받아서 사용하는 것을 의미한다.
타입을 비워놓은 상태에서 어떤 타입을 받을 것인지 호출할 때 정의하는 것이다.
function logText<T>(text: T): T {
console.log(text);
return text;
}
// 호출할 때 타입을 지정 가능
const str = logText<string>("abc");
const login = logText<boolean>(true);
여러 타입을 사용하고자 한다면 유니온 타입을 사용하면 될 텐데, 왜 제네릭을 사용하는 걸까?
함수의 파라미터를 유니온 타입으로 받을 시, 인풋에 대한 문제는 해결되지만 아웃풋에 대한 문제가 발생하게 된다.
function logText(text:string|number){
console.log(text);
return text
}
const hello = logText('hello world')
hello.split(''); ❌
위의 경우 변수 hello
는 유니온 타입으로 string과 number를 받을 수 있다. 하지만 변수를 만들고 메서드를 사용할 때에는 변수가 둘 중 어떤 타입인지 인식하지 못해 string과 number타입에서 모두 가능한 메서드만 사용이 가능해진다.
따라서 string타입으로 변수를 선언하더라도 string타입의 메서드인 split()
을 사용하지 못한다.
하지만 제네릭으로 string타입의 변수를 선언하면 온전히 string타입에 대한 메서드를 모두 사용할 수 있게 된다.
function logText<T>(text: T): T {
console.log(text);
return text;
}
const str = logText<string>("abc");
str.split(''); ⭕️
제네릭이 많이 쓰이는 요소 중 하나로 API의 응답 데이터의 규격을 정의할 때 많이 쓰인다. Promise 자체가 제네릭을 받도록 되어있으므로 함수의 반환값의 타입으로는 Promise<제네릭> 으로 설정해주어야 한다.
function fetchItems():Promise<string[]> {
let items = ['a', 'b', 'c'];
return new Promise((resolve) => {
resolve(items);
});
}
쉽게 말해 API로 비동기적으로 데이터를 받게 된다면, 어떠한 형식으로 받을지 모르기 때문에 리턴 타입을 제네릭방법으로 지정해줘야 하는 것이다.
다음과 같이 인터페이스 형식에도 제네릭을 사용할 수 있다.
interface Dropdown<T> {
value: T;
selected: boolean;
}
const obj1: Dropdown<number> = { value: 10, selected: true };
const obj2: Dropdown<string> = { value: "abc", selected: true };
파라미터로 제네릭을 받는 함수는 내부 코드에 에러를 일으킬 수 있다.
해당 함수가 호출되기 전까지는 어떤 타입이 지정될 지 모르기 때문이다.
function logTextLength<T>(text: T): T {
console.log(text.length); //❌ 호출하기 전에는 어떤 타입인지 지정되지 않아서 에러!
return text;
}
logTextLength("hello");
하지만 제네릭에 타입을 제한하면 제한된 타입의 메서드는 사용이 가능해진다.
아래의 코드는 파라미터의 타입을 배열로 제한한 예시이다.
function logTextLength<T>(text: T[]): T[] {
//호출되기 전에 타입을 배열로 제한해서 내부 함수에 에러를 없앨 수 있다.
console.log(text.length);
return text;
}
logTextLength<string>(["hello"]);
length
메서드가 있는 타입만 지정할 수 있도록 다음과 같이 제네릭의 타입을 제한할 수 있다.
function logTextLength<T extends { length: number }>(text: T): T {
text.length;
return text;
}
toString
메서드가 있는 타입만 지정하도록 하려먼 다음과 같이 제네릭의 타입을 제한할 수 있다.
function createDropdownItem<T extends { toString: Function }>(item: DropdownItem<T>
) {
const option = document.createElement("option");
option.value = item.value.toString();
option.innerText = item.value.toString();
option.selected = item.selected;
return option;
}
정의되어 있는 인터페이스의 키값을 제네릭의 타입으로 제한하여 코드를 짤 수도 있다.
제네릭에 extends keyof <인터페이스 명>
의 형식으로 가능하다.
interface ShoppingItem {
name: string;
price: number;
stock: number;
}
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
return itemOption;
}
getShoppingItemOption("price"); //⭕️
getShoppingItemOption(price); //❌
getShoppingItemOption(15443); //❌
파라미터는 ShoppingItem
의 키 값(name, price, stock) 중 한가지만 들어올 수 있다.