wam
w__am 개발노트
wam
  • 분류 전체보기 (165)
    • CS 지식 (10)
      • 자료구조 (0)
      • 알고리즘 (0)
      • 컴퓨터 구조 (0)
      • 운영체제 (0)
      • 네트워크 (7)
      • 데이터베이스 (0)
      • 디자인 패턴 (3)
    • Frontend (131)
      • Three.js (64)
      • NPM (1)
      • Nest.js (19)
      • React (10)
      • Apollo (7)
      • TypeScript (2)
      • JavaScript (12)
      • HTML, CSS (1)
      • Jest (3)
      • E2E (5)
      • Cypress (7)
    • Database (12)
      • TypeORM (12)
    • IT 지식 (8)
      • 클라우드 서비스 (3)
      • 네트워크 (1)
      • 데이터 포맷 (2)
      • 기타 (2)
    • IT Book (2)
    • 유용한 사이트 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록
  • 🐱 Github

인기 글

태그

  • 삼각함수
  • getelapsedtime()
  • 디자인 패턴
  • gridhelper
  • 오프-프레미스(off-premise) 방식
  • 함수 표현식
  • 렌더링 성능 최적화
  • axeshelper
  • mapped types
  • math.sin()
  • 원형적인 움직임
  • math.cos()
  • Interface
  • getdelta()
  • 스코프
  • e.preventdefault()
  • Decorators
  • type-graphql
  • threejs 개발 할 때 도움을 줄 수 있는 유틸리티
  • 초기 환경설정
  • 함수 선언문
  • isabstract
  • API
  • three.js 구성 요소
  • reactive variables
  • 함수 리터럴
  • 함수의 범위
  • react 성능 최적화
  • joi 에러
  • 데이터 포맷

최근 글

관리자

글쓰기 / 스킨편집 / 관리자페이지
hELLO · Designed By 정상우.
wam

w__am 개발노트

제네릭
Frontend/TypeScript

제네릭

2023. 8. 18. 05:20

제네릭 사전적 정의

  • 제네릭은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징
  • 특히, 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는 데 사용

제네릭 기본 문법이 적용된 형태

function logText<T>(text: T): T {
  return text;
}
  • 제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미
logText<string>('hi');
logText<number>(10);
logText<boolean>(true);
  • 함수를 호출할 때 파라미터의 대한 타입을 인자값에 지정해서 호출한다.
  • getText<string>('hi');
    getText 함수는 아래와 같이 타입을 정의한 것과 같다.
function getText<string>(text: string): string {
  return text;
}
  • 위 함수는 입력 값의 타입이 string이면서 반환 값 타입도 string이어야 한다

 

 

제네릭을 사용하는 이유

function logText(text: any): any {
  return text;
}
  • 위와 같이 any를 사용할 수 있지만 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없다. → any라는 타입은 타입 검사를 하지 않기 때문
function logText<T>(text: T): T {
  return text;
}
  • 함수의 인자와 반환 값에 모두 T라는 타입을 추가
  • 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 된다.
  • 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 된다.
// #1
const text = logText<string>("Hello Generic");
// #2
const text = logText("Hello Generic");
  • 보통 두 번째 방법이 코드도 더 짧고 가독성이 좋기 때문에 흔하게 사용
  • 만약 복잡한 코드에서 두 번째 코드로 타입 추정이 되지 않는다면 첫 번째 방법을 사용하기

 

 

기존 타입 정의 방식과 제네릭의 차이점 - 함수 중복 선언의 단점

function logText(text: string) {
  return text;
}

function logNumber(num: number) {
  return num;
}
  • 위와 같이 타입만 다른데 함수를 만드는 것은 좋지 않다. 제네릭 방식으로 처리하자.

 

 

기존 문법과 제네릭의 차이점 - 유니온 타입을 이용한 선언 방식의 문제점

유니온 타입을 이용한 선언 방식

function logText(text: string | number {
  return text;
}

문제점 (반환값에 대해 문제가 생긴다.)

  • string과 number가 공통으로 접근할 수 있는 속성이나 API만 사용이 가능해진다.

 

 

제네릭의 장점과 타입 추론에서의 이점

유니온 타입을 이용한 선언 방식 대신 제네릭으로 사용

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

const str = logText<string>('abc');

str.split(''); // string의 속성을 사용할 수 있음

const login = logText<boolean>(true); // // 타입의 이점

 

  • 호출하는 시점에서 타입을 정할 수 있다.
  • 타입을 추론해서 반환값까지 붙일 수 있다.

 

 

제네릭 실전 예제

예제 HTML

<!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>이메일 선택 드롭다운</h1>
      <select id="email-dropdown">
        <option value="naver.com" selected>naver.com</option>
        <option value="google.com">google.com</option>
        <option value="hanmail.net">hanmail.net</option>
      </select>
    </div>
    <div>
      <h1>상품 수량 선택 드롭다운</h1>
      <select id="product-dropdown">
        <option value="1" selected>1</option>
        <option value="2">2</option>
        <option value="3">3</option>
      </select>
    </div>
  </body>
</html>

1) 기본

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);
});

2) 코드에 타입 정의

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: Email | 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);
});

numberOfProducts.forEach(function (Product) {
  const item = createDropdownItem(Product);
});

 

2) 인터페이스에 제네릭을 선언하는 방법 (2번의 타입 정의를 제네릭으로 선언하기)

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;
}

// NOTE: 이메일 드롭 다운 아이템 추가
emails.forEach(function (email) {
  const item = createDropdownItem(email);
  const selectTag = document.querySelector("#email-dropdown");
  selectTag.appendChild(item);
});

numberOfProducts.forEach(function (Product) {
  const item = createDropdownItem(Product);
});

2-1) 제네릭의 타입을 문자와 숫자만 받게끔 유니온 타입으로 제한

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<T extends string | number>(item: DropdownItem<T>) {
  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<string>(email);
  const selectTag = document.querySelector("#email-dropdown");
  selectTag.appendChild(item);
});

numberOfProducts.forEach(function (Product) {
  const item = createDropdownItem<number>(Product);
});

2-2) 제네릭의 타입을 toString()이라는 메서드를 가진 타입으로 제한

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<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;
}

// NOTE: 이메일 드롭 다운 아이템 추가
emails.forEach(function (email) {
  const item = createDropdownItem<string>(email);
  const selectTag = document.querySelector("#email-dropdown");
  selectTag.appendChild(item);
});

numberOfProducts.forEach(function (Product) {
  const item = createDropdownItem<number>(Product);
});
  • 제네릭으로 받을 수 있는 타입을. toString() 함수가 있는 타입으로 제한하는 것
  • toString() API는 객체의 내장 API이다.
  • 해당 객체를 프로토 타입으로 받고 있는 문자, 숫자, 불린 등 주요 타입들에도 다 toString() API가 기본적으로 제공된다.
  • 2-1번 코드보다는 문자, 숫자 이외에 더 많은 타입을 수용할 수 있는 형태가 된다.

 

 

제네릭의 타입 제한

logTextLength 함수는 제네릭 타입이다.

여기서 text.length 는 사용할 수가 없다.

함수 호출 시 타입이 정해지기 때문에 미리 문자가 올 것이라 추론하지 못하기 때문이다.

function logTextLength<T>(text: T[]): T[] {
  console.log(text.length); // 2

  text.forEach((text) => {
    console.log(text);
  });

  return text;
}

logTextLength<string>(["hello", "hi"]);

제네릭을 선언하고 length를 사용하려면 배열로 만들어줘야 한다.

여기서의 length는 배열의 개수이며 글자의 길이가 아니다.

글자의 length를 사용할 때 아래처럼 해주었다.

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

const logText = logTextLength<string>("hello");
console.log(logText.length)

 

 

정의된 타입으로 타입을 제한하기

제네릭을 사용한 함수 안에 length 사용하는 방법은 인터페이스를 사용해 주기.

interface LengthType {
  length: number;
}

function logTextLength2<T extends LengthType>(text: T): T {
  text.length;
  return text; // 반환값에 text.length를 사용할 수 없다.
}

logTextLength2("abc"); // 사용가능
logTextLength2(10); // 사용 불가능 - 숫자에는 length 속성이 없다.
logTextLength2({ length : 20}); // 사용가능

 

 

keyof로 제네릭의 타입 제한하기

interface ShoppingItems {
  name: string;
  price: number;
  address: string;
  stock: number;
}
function getAllowedOptions<T extends keyof ShoppingItems>(option: T): T {
	return option;
}

getAllowedOptions(10); // X
getAllowedOptions<string>('a'); // X
getAllowedOptions('name'); // O | ShoppingItems 인터페이스의 key 값만 사용이 가능

 

 

 

저작자표시 변경금지 (새창열림)

'Frontend > TypeScript' 카테고리의 다른 글

타입 별칭과 인터페이스 차이점  (0) 2023.03.22
    'Frontend/TypeScript' 카테고리의 다른 글
    • 타입 별칭과 인터페이스 차이점
    wam
    wam

    티스토리툴바