[인프런 타입스크립트 입문 - 기초부터 실전까지] - 제네릭

Lee Jeong Min·2021년 12월 12일
0

TypeScript

목록 보기
6/18
post-thumbnail

이 글은 타입스크립트 입문 - 기초부터 실전까지의 제네릭을 보고 정리한 글입니다.

제네릭 소개

제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다.

제네릭 | 타입스크립트 핸드북

제네릭의 기본 문법

// 일반 함수형
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이 Email인 경우가 있고, 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);
});

아까 만들었던 EmailProductNumber라는 인터페이스들을 제거하고 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 프로퍼티를 가지고 있지 않기 때문에 에러가 발생한다.

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

// 제네릭의 타입 제한 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라는 예약어를 사용하여 제네릭에 올 수 있는 인수로 인터페이스에 정의된 속성들만(키 값들만) 오게 만들 수 있다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글