본문으로 바로가기

ts 핸드북 : (https://typescript-kr.github.io/pages/generics.html)

한눈에 보는 타입스크립트 : (https://heropy.blog/2020/01/27/typescript/)

generic에 대해 잘 설명한 포스트 : (infoscis.github.io/2017/05/25/TypeScript-handbook-generic/)

 

 

Generic 사용법

사실 Generic은 java와 같은 다른 언어에서 이미 사용되어 온 특징이다.

 

Generic은 재사용을 목적으로 함수나 클래스의 선언 시점이 아닌, 사용(호출) 시점에 타입을 선언할 수 있게 됩니다.

 

제네릭을 사용하게 되면 다음의 장점을 누릴 수 있습니다.

(1) 재사용하므로 타입 정의 코드를 줄여나갈 수 있다.

(2) 재사용을 위해 any 범벅을 만들지 않아도 된다. any는 헬퍼의 도움을 받을 수 없지만 제네릭은 타입을 부여해주므로 헬퍼가 작동하므로 generic을 쓰는 것이 훨씬 좋다.

// generic사용. 재사용 가능하면 타이핑도 들어가 자동 완성 해 줌
function logText<T>(param: T): T {
  return param;
}

// any 사용. 재사용은 할 수 있으나 타이핑이 안 됨
function logText(param: any): any {
  return param;
}

 

꼭 함수뿐만 아니라 인터페이스, 클래스, 함수, 타입 별칭 등에 사용할 수 있습니다.

<> 안의 문자는 꼭 T가 아니어도 됩니다. Type의 줄임말로 관습적으로 T를 사용할 뿐입니다.

// 함수에서 사용
function identity<T>(arg: T): T {
  return arg;
}

// interface에서 사용
interface IValue<T> {
  value: T;
}

// 클래스에서 사용
class Valuable<T> {
  constructor(public value: T) {}
}

 

 

generic에 호출 시점에서의 타입 명시

함수를 정의하는 부분에서는 <T> 제네릭을 통해 타입을 선언하지 않았지만 <> 내에 특정 타입을 지정함으로써 타입을 지정할 수 있습니다. 보통 이 방식을 많이 사용하죠. 물론 지정하지 않아도 타입을 추론해서 적절한 제레닉 타입을 찾아냅니다.

function identity<T>(arg: T): T {
  return arg;
}

// 타입 지정
console.log(identity<number>(3));
console.log(identity<string>("coding"));

// 타입 추론 해 줌
console.log(identity([1, 3, 5]));

 

리턴하는 타입이 명시된다는 것이 왜 중요하냐면, return된 값으로 활용할 때 헬퍼가 작동하기 때문입니다.

function logText<T>(text: T): T {
  console.log(text);
  return text;
}

const sttt = logText<string>("hello"); // string이구나
console.log(sttt.split("")); // string이므로 split 메서드 헬퍼가 작동함

 

 

interface, class에 generic 사용

 

클래스나 인터페이스도 마찬가지로 제네릭을 활용할 수 있으므로 다음과 같이 작성할 수 있습니다.

 

interface DropDown<T> {
  value: T;
  selected: boolean;
}

const obj: DropDown<number> = {
  value: 1,
  selected: false,
};
class Human<T, K> {
  constructor(private _name: T, public age: K) {}
  say(): void {
    console.log(`${this._name} is ${this.age} years old`);
  }
}

const me = new Human<string, number>("foo", 100);
console.log(me);

 

 

실제 제네릭 사용 예시

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>        
        <h1>이메일 dropdown</h1>
        <select name="email-dropdown" id="email-dropdown">
            <!-- <option value="naver.com">naver.com</option>
            <option value="google.com">google.com</option>
            <option value="hanmail.net">hanmail.net</option> -->
        </select>
    </div>
    <div>
        <h1>상품 수량</h1>
        <select name="product-dropdown" id="product-dropdown">
            <!-- <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option> -->
        </select>
    </div>
    <script src="./drop.js"></script>
</body>
</html>
interface DropDownItem<T> {
  value: T;
  selected: boolean;
}

const emails: DropDownItem<string>[] = [
  { value: "naver.com", selected: true },
  { value: "gmail.com", selected: false },
  { value: "hanmail.com", selected: false },
];

const numberOfProduct: DropDownItem<number>[] = [
  { value: 1, selected: true },
  { value: 2, selected: false },
  { value: 3, selected: false },
];

// toString을 메서드를 가지고 있는 객체를 extends하여 에러를 해결
function createDropdownItem<T extends { toString: Function }>(
  item: DropDownItem<T>
): HTMLOptionElement {
  const option = document.createElement("option");
  option.value = item.value.toString();
  option.innerText = item.value.toString();
  option.selected = item.selected;
  return option;
}

// 배열에 foreach를 돌려가며 appendchild해 줌.
emails.forEach(function (email: DropDownItem<string>) {
  const item = createDropdownItem<string>(email);
  const selectTag = document.querySelector("#email-dropdown");
  selectTag.appendChild(item);
});

numberOfProduct.forEach(function (productNumber: DropDownItem<number>) {
  const item = createDropdownItem<number>(productNumber);
  const selectTag = document.querySelector("#product-dropdown");
  selectTag.appendChild(item);
});

 

 

Generic 타입 제한

 

앞서 아래와 같이 Generic에 extends {toString: Function}을 붙여서 특정 메서드를 가진 타입으로만 가능하도록 제한한 부분이 있습니다.

toString 메서드를 가진 타입만 사용하도록 만들 수 있습니다.

function createDropdownItem<T extends { toString: Function }> ... 중략

 

비슷하게, length 메서드가 있는 타입만 지정할 수 있도록 generic을 제한하는 방법은 다음과 같습니다.

아래 예시는 length 메서드가 있는 타입으로 제한하였습니다.

function logTextLenghth<T extends { length: number }>(text: T): number {
  return text.length;
}

// 잘 안보이면 interface로 분리해보자
interface LengthType {
  length: number;
}

function logTextLenghth<T extends LengthType>(text: T): number {
  return text.length;
}

 

추가로, keyof를 사용하여 특정 객체의 키값만을 T로 넣을 수 있게할 수 있습니다.

아래 예시는 itemOption에는 name, price, stock만 올 수 있습니다. keyof에 대해 알고 계시면 이해할 수 있습니다.

interface ShoppingItem {
  name: string;
  price: number;
  stock: number;
}

// ShoppingItem의 키 중에서 한가지가 Generic이 된다.
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
  return itemOption;
}

console.log(getShoppingItemOption("name"));
console.log(getShoppingItemOption("price"));
console.log(getShoppingItemOption("stock"));

 

typescript에서 js querySelector를 정의한 부분을 사렾보신다면 keyof를 요긴하게 사용한 것을 확인하실 수 있습니다.

querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;

 


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체