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

EJ·2020년 11월 24일
0

Typescript

목록 보기
11/25
post-thumbnail

제네릭

타입을 마치 함수의 파라미터 개념인 것처럼 받게 되는 것이다.

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


제네릭의 기본 문법

function logText<T>(text: T):T {
	console.log(text);
	return text;
}
logText('hi');
  • 기본적으로 파라미터(text)의 타입은 'hi'가 된다.
  • 문자열을 그대로 들고가서 한 바퀴(console.log(), return) 돈 뒤에 반환하는 것까지도 문자열이 된다고 정의할 수 있다.
  • 즉, 호출하는 시점에 문자열, 숫자 등의 타입을 넘겨줄 수 있는 것이 제네릭이다.
logText<string>('hi');
  • 명시적으로 string이라는 타입을 넘겨줬다.
  • 따라서, logText()의 안에서 처리하는 text에 대한 타입은 위에서 넘겼던 문자열(string)이 된다.

기존 타입 정의 방식과 제네릭의 차이점

1. 함수 중복 선언의 단점

function logText(text) {
	console.log(text);
	return text;
}
logText('a');
logText(10);
logText(true);
  • 문자열, 숫자, 를 넣었을 때 전부 다 받을 수 있는 이유는 현재 logText()함수에서 타입을 정의하지 않아 암묵적으로 타입이 any라고 되어있기 때문에 어떤 타입도 받을 수 있는 것이다.

function logText(text: string) {
	console.log(text);
	// text.split('').reverse().join(''); // 타입이 string일때 가능.
	return text;
}
function logNumber(num: number) {
	console.log(num);
	return num;
}
logText('a');
logText(10);
logNumber(10);
logText(true);
  • 위의 경우처럼 타입을 다르게 받기 위해 중복되는 코드들을 반복해서 사용하는 것은 코드관점, 유지보수 관점에서 좋지 않다.
  • 이런 것들을 해결하기 위해 유니온 타입을 사용할 수 있다.

2. 유니온 타입을 이용한 선언 방식의 문제점

  • 아래의 코드는 유니온 타입을 사용해 string타입과 number타입 모두 가능하도록 해준 상태이다.
function logText(text: string | number) {
	console.log(text);
	return text;
}
logText('a');
logText(10);

const a = logText('a');
a.split('') // split에 빨간 밑줄 에러가 발생한다.
  • string타입과 number타입 모두 적어줬지만, 문자열을 넣어줘도 string에서 제공하는 split이 에러가 난다.

  • 타입이 string | number에서는 string을 제공하지 않기 때문이다. 즉, 정확한 타입을 넣어줘야만 사용가능하다는 문제점이 생기는 것이다.

  • 인풋값은 해결되었지만, 반환값은 해결되지 않아서 문제가 발생한는 것이다.


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

제네릭은 타입 정의에 대한 이점을 가지고 있다. 함수를 정의할 때 타입을 비워놓고, 해당 함수를 호출한 시점에 타입을 정의하는 것이 제네릭이다.
제네릭을 사용하면 타입을 추론을 해서 최종 반환값까지 정의할 수 있다.

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

// string이라는 타입을 받아서 쓰겠다고 호출할 때 정의하는 것이다.
// 파라미터(인자)와 반환값이 모두 string에 될 것이라고 타입스크립트 내부에서 제네릭을 이용해서 선언한 것이다.
const str = logText<string>('abc');
str.split(''); // string에 사용하는 split을 사용할 수 있다.

// 위의 함수의 같은 함수인 logText함수를 호출시 boolean을 타입으로 정의했다.
const login = logText<boolean>(true);

인터페이스에 제네릭을 선언하는 방법

// 인터페이스에 제네릭을 선언하는 방법
interface Dropdown {
	   value: string;
	   selected: boolean;
}

const obj: Dropdown = { value: 'abc', selected: false };
const obj: Dropdown = { value: 10, selected: false }; // value값에는 string이라는 타입이 정의되어있기 때문에 에러가 발생한다.
  • Dropdown인터페이스valuestring라고 정의했기 때문에 value에 숫자를 사용할 경우 에러가 발생하게 된다.

이처럼 여러 타입을 사용하기 위해 인터페이스 선언이 늘어나게 되고 코드가 지저분해질 경우, 제네릭을 사용해 손쉽게 선언할 수 있다.

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

const obj:Dropdown<string> = { value: 'abc', selected: false };
const obj:Dropdown<number> = { value: 10, selected: false };
  • 제네릭을 이용하면 여러 타입의 인터페이스를 손쉽게 사용할 수 있다.
  • Dropdown이라는 인터페이스를으로 제네릭으로 정의하는데, 정의를 할 때 타입을 선언하는 시점에 타입을 추가적으로 넘겨서 Dropdown인터페이스의 타입을 바꿔준다.

제네릭 실전 예제

프로젝트 실습 파일:
실습자료 - learn-typescript - example - dropdown-generic.html, dropdown-generic.ts


코드에 타입 정의하기

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);
})
  • 타입마다 인터페이스로 정의해서 유니온 방식으로 작성한 코드이다.
  • 이렇게 작성하면 타입이 늘어날 때마다 코드가 길어지게 된다. 따라서, 제네릭을 사용해 유연하게 작성하면 좋다.

제네릭을 이용한 타입 정의

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>(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);
})
  • interface에 제네릭을 이용해 여러 타입이 가능하도록 했다.
  • createDropdownItem()함수에도 제네릭을 이용함으로써 유니온 방식을 사용하지 않고 타입 정의가 가능해졌다.

하나의 인터페이스로 제네릭을 이용해 여러가지 타입을 커버해 타입코드를 줄여나갈 수 있다는 것이 제네릭의 장점이다.


제네릭의 타입 제한

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

logTextLength('hi');
  • logTextLength('hi)를 실행할 경우 console.log(text.length)의 length에 빨간 밑줄이 생기게 된다.
  • logTextLength()에 어떤 타입이 들어올지 알 수 없기 때문에 length가 있다는 것은 개발자만 알고 있는 상태이다. (타입스크립트는 모름)

  • length를 사용하려면 <T>라는 제네릭타입을 제한을 해야 한다. 힌트를 줘야 한다.
function logTextLength<T>(text: T[]): T[] {
	   console.log(text.length);
	   text.forEach(function(text) {
	       console.log(text);       
	   });   
	   return text; 
}

logTextLength<string>(['hi', 'abc']);

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

extends
클래스와 인터페이스를 다룰 때 상위에 이미 정의되어 있는 타입들을 확장할 경우 사용한다.

제네릭으로 정의된 타입을 특정 타입으로 제한하면 특정 타입의 하위 속성이 된다.
특정 타입에서 제공되는 속성을 기본적으로 가진 상태에서 추가로 속성을 더 정의할 수 있게 된다.

interface LengthType {
    length: number;
}

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

logTextLength('a');
logTextLength({ length: 10 });

logTextLength(10); // number인 10은 사용 불가하다.
// 제네릭에 어느정도 타입 제한이 생기는 것을 볼 수 있다.
  • extends를 이용해 <T>의 타입을 LengthType라는 타입으로 제한했다.
  • 따라서 LengthTypelength라는 속성을 이용할 수 있게 된다.

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

선언된 인터페이스의 속성 중 하나만 받겠다고 제약할 수 있다.

interface ShoppingItem {
    name: string;
    price: number;
    stock: number;
}
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
    return itemOption;
}
getShoppingItemOption('name');

💡참고

profile
주니어 프론트엔드 개발자 👼🏻

0개의 댓글