이 글은 타입스크립트 입문 - 기초부터 실전까지의 제네릭을 보고 정리한 글입니다.
제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다.
// 일반 함수형
function logText<T>(text: T): T {
console.log(text);
return text;
}
// 화살표 문법
const logText = <T>(text: T): T => {
console.log(text);
return text;
}; // tsx에선 <T extends unknown>와 같이 extends를 붙여주어야 한다고 함
logText('하이'); // 1
logText<string>('하이'); // 2
함수에서 제네릭을 사용하려면 다음과 같이 작성하여 사용할 수 있다.
logText('하이')
라는 함수 호출 시, 함수는 내부적으로 타입을 추론하여 string 타입을 인식하여 작동하낟.
logText<string>('하이')
는 제네릭을 사용하여 직접호출한 것으로 string이라는 타입을 제네릭으로 넘겨 사용하여 vscode에서 타입이 무엇인지 바로 보여준다.
function logText(text: string) {
console.log(text);
return text;
}
function logNumber(num: number) {
console.log(num);
return num;
}
logText('a');
logText(10);
logNumber(10);
logText(true);
기존 타입 정의 방식으로 함수를 정의하였을 시, 타입에 따라 다른 함수들을 만들기 때문에 console.log 부분과 return문이 계속해서 중복된다는 사실을 확인할 수 있다.
function logText(text: string | number) {
console.log(text);
return text;
}
logText('a');
logText(10);
들어가는 인풋에 대한 타입처리는 가능하지만 함수 안에서 파라미터의 메서드를 보면 string과 number의 교집합의 메서드를 vscode에서 추천해줌
또한 반환값을 받아서도 여전히string|number
타입이기 때문에 여기서도 교집합이 되는 메서드를 추천해준다.
function logText<T>(text: T): T {
console.log(text);
return text;
}
const str = logText<string>('abc');
str.split('');
const login = logText<boolean>(true);
아까 제네릭을 사용하지 않은 함수들에서 보았던 문제들을 제네릭을 사용하면 해결할 수 있다. (반환값과 함수안의 타입을 하나로 해주는 것)
제네릭을 사용하여 호출할 시점에 타입 정의!
예제를 설명하는 부분이라 맥락 설명은 생략하겠습니다. --> 자세한 건 강의에서
const emails = [
{ value: 'naver.com', selected: true },
{ value: 'gmail.com', selected: false },
{ value: 'hanmail.net', selected: false },
];
const numberOfProducts = [
{ value: 1, selected: true },
{ value: 2, selected: false },
{ value: 3, selected: false },
];
function createDropdownItem(item) {
const option = document.createElement('option');
option.value = item.value.toString();
option.innerText = item.value.toString();
option.selected = item.selected;
return option;
}
// NOTE: 이메일 드롭 다운 아이템 추가
emails.forEach(function (email) {
const item = createDropdownItem(email);
const selectTag = document.querySelector('#email-dropdown');
selectTag.appendChild(item);
});
처음 코드를 보면 일반 JS문법처럼 아무것도 적혀있지 않다. 이를 ts 문법으로 바꾸게 되면 다음과 같아진다.
interface Email {
value: string;
selected: boolean;
}
const emails: Email[] = [
{ value: 'naver.com', selected: true },
{ value: 'gmail.com', selected: false },
{ value: 'hanmail.net', selected: false },
];
interface ProductNumber {
value: number;
selected: boolean;
}
const numberOfProducts: ProductNumber[] = [
{ value: 1, selected: true },
{ value: 2, selected: false },
{ value: 3, selected: false },
];
function createDropdownItem(item: ProductNumber) {
const option = document.createElement('option');
option.value = item.value.toString();
option.innerText = item.value.toString();
option.selected = item.selected;
return option;
}
// NOTE: 이메일 드롭 다운 아이템 추가
emails.forEach(function (email) {
const item = createDropdownItem(email); // 에러 발생
const selectTag = document.querySelector('#email-dropdown');
selectTag.appendChild(item);
});
이렇게 작성을 하게 되면,
createDropdownItem
의 인수로 들어가는 item이ProductNumber
인 경우가 있기 때문에 에러가 발생한다.
--> 유니온 타입으로 바꾸기
...
function createDropdownItem(item: Email | ProductNumber) {
...
이렇게 바꾸면 문제가 해결된 것처럼 보이지만, item이라는 값에 새로운 값들이 들어올 때마다, 인터페이스들을 정의하여 유니온 타입으로 작성해주어야하기 때문에 확장가능성에 맞지 않는다. --> 인터페이스 제네릭 활용!
// 인터페이스에 제네릭을 선언하는 방법
interface Dropdown<T> {
value: T;
selected: boolean;
}
const obj: Dropdown<string> = { value: 'abc', selected: false };
인터페이스에 제네릭을 사용하기 위해
<T>
파라미터를 넘겨받아value
에 할당해준다. 그리고 이를 사용할때, 타입 인수를 넘겨주면 된다.
interface DropdownItem<T> {
value: T;
selected: boolean;
}
// interface Email {
// value: string;
// selected: boolean;
// }
const emails: DropdownItem<string>[] = [
{ value: 'naver.com', selected: true },
{ value: 'gmail.com', selected: false },
{ value: 'hanmail.net', selected: false },
];
// interface ProductNumber {
// value: number;
// selected: boolean;
// }
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;
}
// NOTE: 이메일 드롭 다운 아이템 추가
emails.forEach(function (email) {
const item = createDropdownItem(email);
const selectTag = document.querySelector('#email-dropdown');
selectTag.appendChild(item);
});
아까 만들었던
ProductNumber
라는 인터페이스들을 제거하고DropdownItem
이라는 인터페이스에 제네릭을 사용하여 만들면 위 코드와 같이 만들 수 있다.
유니온 타입을 제거하여 createDropdownItem
를 정의하면 아래와 같이 작성할 수 있다.
// 정의
function createDropdownItem<T>(item: DropdownItem<T>)
// 호출 시
createDropdownItem<string>(email)
createDropdownItem<number>(product)
// 제네릭의 타입 제한
function logTextLength<T>(text: T): T {
console.log(text.length);
return text;
}
logTextLength<string>(['hi', 'abc']);
위 코드에선 다음과 같은 에러를 확인할 수 있다.
이는 타입스크립트가 logTextLength
라는 현재 정의된 인수에 어떤 타입이 들어올지 모르기 때문에 발생하는 것이다.
이를 위해 제네릭 T에 []
배열 리터럴을 주게 되면 문제가 해결된다.
// 제네릭의 타입 제한
function logTextLength<T>(text: T[]): T[] {
console.log(text.length);
text.forEach(text => {
console.log(text);
});
return text;
}
logTextLength<string>(['hi', 'abc']);
따라서 타입 제한이란 제네릭에 추가적인 타입 힌트를 주는 방법이다.
// 제네릭 타입 제한 2 - 정의된 타입 이용하기
interface LengthType {
length: number;
}
function logTextLength<T extends LengthType>(text: T): T {
text.length;
return text;
}
logTextLength('a');
logTextLength({length: 10});
logTextLength(10); // 에러 발생
정의된 interface를 활용하여
extends
키워드와 같이 제네릭을 정의하면 타입을 제한할 수 있다.
문자열 a의 경우 length라는 프로퍼티를 가지고 있고, 객체로 전달한 것 또한 length라는 프로퍼티를 가지고 있지만, 숫자 10은 length 프로퍼티를 가지고 있지 않기 때문에 에러가 발생한다.
// 제네릭의 타입 제한 3 - keyof
interface ShoppingItem {
name: string;
price: number;
stock: number;
}
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
return itemOption;
}
// getShoppingItemOption(10);
// getShoppingItemOption<string>('a');
getShoppingItemOption('name');
keyof라는 예약어를 사용하여 제네릭에 올 수 있는 인수로 인터페이스에 정의된 속성들만(키 값들만) 오게 만들 수 있다.