제네릭이란 타입을 마치 함수의 파라미터처럼 사용하여 재사용성이 높은 컴포넌트를 만들 때 활용 특히 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용.
function logText(text) {
console.log(text);
return text;
}
logText(10);
logText('string');
logText(true);
function logText(text: string) {
console.log(text);
text.split('').reverse().join('');
return text;
}
function logNumber(num: number) {
console.log(num);
return num;
}
function logBoolean(bool: boolean) {
console.log(bool);
return bool;
}
단순히 다른 타입을 인자로 받기 위해서 중복되는 코드를 생산해 나가는것은 가독성 뿐만 아니라 유지보수 관점에서도 좋지 않다.
function logText(text: string | number | boolean) {
console.log(text);
//문제1 text.{string, number, boolean 타입의 공통적인 속성만 사용 가능}
text.valueOf();
text.toLocaleString();
text.toString();
return text;
}
// 문제2 vsc의 랭귀지 서버에서는 리턴값의 타입을 정확히 추론하지 못하기 때문에
// 아래와 같이 스트링 타입 인자를 통해 스트링 타입의 결과를 받더라도
// 스트링의 내장 함수 사용 시 에러가 발생된다.
const a = logText('a');
a.split(''); // 에러 발생
function logText2<T>(text: T):T {
console.log(text);
return text;
}
const str = logText2<string>('string');
str.split('')
const num = logText2<number>(10);
num.toFixed();
const bool = logText2<boolean>(false);
if(bool) console
제네릭을 사용하면 위와같이 인자의 타입을 여러가지 타입으로 적용하여 함수를 재활용할 수 있다.
interface Dropdown<T> {
value: T;
selected: boolean;
}
const obj1: Dropdown<string> = { value: 'string', selected: false};
const obj2: Dropdown<number> = { value: 1000, selected: false};
const obj3: Dropdown<boolean> = { value: true, selected: false};
인터페이스에 타입을 넘겨주어 값에 해당하는 타입을 유동적으로 적용시킬 수 있다.
interface Email {
value: string;
selected: boolean;
}
interface ProductNumber {
value: number;
selected: boolean;
}
const emails: Email[] = [
{ value: 'naver.com', selected: true },
{ value: 'gmail.com', selected: false },
{ value: 'hanmail.net', selected: false },
];
const numberOfProducts: ProductNumber[] = [
{ value: 1, selected: true },
{ value: 2, selected: false },
{ value: 3, selected: false },
];
function createDropdownItem(item: Email | ProductNumber) {
const option = document.createElement('option');
option.value = item.value.toString();
option.innerText = item.value.toString();
option.selected = item.selected;
return option;
}
emails.forEach((email) => {
const item = createDropdownItem(email);
const selectTag = document.querySelector('#email-dropdown');
selectTag.appendChild(item);
});
numberOfProducts.forEach((product) => {
const item = createDropdownItem(product);
const selectTag = document.querySelector('#product-dropdown');
selectTag.appendChild(item);
})
createDropdownItem()에 유니온을 사용하여 두 개의 인터페이스를 인자 타입으로 사용할 수 있게하였다. 결과적으로 타입이 다른 emails, numberOfProducts 배열을 루프시켜 createDropdownItem() 함수를 타입에 관계없이 활용할 수 있다.
// interface Email {
// value: string;
// selected: boolean;
// }
// interface ProductNumber {
// value: number;
// selected: boolean;
// }
interface DropdownItem<T> {
value: T;
selected: boolean;
}
const emails: DropdownItem<string>[] = [
{ value: 'naver.com', selected: true },
{ value: 'gmail.com', selected: false },
{ value: 'hanmail.net', selected: false },
];
const numberOfProducts: DropdownItem<number>[] = [
{ value: 1, selected: true },
{ value: 2, selected: false },
{ value: 3, selected: false },
];
function createDropdownItem(item: DropdownItem<string> | DropdownItem<number>) {
const option = document.createElement('option');
option.value = item.value.toString();
option.innerText = item.value.toString();
option.selected = item.selected;
return option;
}
// 유니온을 사용하지 않고 함수에도 제네릭을 적용시켜 드랍다운 아이템에서도 타입을 참조하게끔 할 수 있다.
// function createDropdownItem<T>(item: DropdownItem<T>) {
// const option = document.createElement('option');
// option.value = item.value.toString();
// option.innerText = item.value.toString();
// option.selected = item.selected;
// return option;
// }
emails.forEach((email) => {
const item = createDropdownItem(email);
// const item = createDropdownItem<string>(email);
const selectTag = document.querySelector('#email-dropdown');
selectTag.appendChild(item);
});
numberOfProducts.forEach((product) => {
const item = createDropdownItem(product);
// const item = createDropdownItem<number>(product);
const selectTag = document.querySelector('#product-dropdown');
selectTag.appendChild(item);
})
타입 제한을 적절히 사용하면 제네릭을 목적하는 곳에 타입을 제한시켜 제네릭을 보다 안전하게 쓸 수 있다.
function logTextLength<T>(text: T): T {
console.log(text.length); // 에러 발생
return text;
}
타입스크립트 입장에서는 text가 어떤 타입인지 알 수 없다. 따라서 .length를 사용 시 에러가 발생된다.
제네릭에 타입을 추가적으로 명시하여 타입 제한
제네릭에 배열 타입을 추가적으로 명시하여 타입에 대한 힌트를 제공함으로써 타입을 제한할 수 있다.
// 배열([]) 타입을 추가적으로 명시
function logTextLength<T>(text: T[]): T {
console.log(text.length);
return text;
}
제네릭에 배열([]) 타입을 추가적으로 명시해주면 타입스크립트는 인자가 배열임을 알 수 있으므로 length 사용 시 에러가 발생되지 않는다.
정의된 타입으로 타입 제한
interface LengthType {
length: number;
}
// 인터페이스를 상속 받는 제네릭으로 명시
function logTextLength<T extends LengthType>(text: T): T {
console.log(text.length);
return text;
}
logTextLength<string>('hi');
logTextLength<number>(10); // number 타입에는 length 속성을 가지고있지 않으므로 오류가 발생한다.
string은 length 속성을 가지고 있다. length 속성을 보장하는 인터페이스를 상속하는 제네릭으로 명시하였으므로 length 속성을 사용할 수 있다. 단, 함수 호출시에 number 타입과 같이 length 속성을 가지고있지 않은 타입으로 호출 시 오류가 발생한다.
keyof로 제네릭의 타입 제한
interface ShoppingItem {
name: string;
price: number;
stock: number;
}
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
return itemOption;
}
getShoppingItemOption('name')
getShoppingItemOption('price');
getShoppingItemOption('stock');
getShoppingItemOption('something') // 오류 발생
keyof 키워드를 사용하여 특정 인터페이스의 정의된 키값으로만 인자를 제한할 수 있다.