에릭말고 제네릭

1. Generics

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

2. 기본 형태

function logContent<T>(content: T): T{
	console.log(content);
	return content;
}

logContent('soryeongk');

선언 시점에 타입을 결정하는 것이 아니라 호출 시점에 타입을 결정할 수 있게 합니다.

T는 Type을 의미하기 위해 작성한 것입니다. 이름은 아무래도 좋습니다. :)

// T를 굳이 안써도 됨
function logContent<soryeongk>(content: soryeongk): soryeongk {
  console.log(content);
  return content;
}

3. 왜 써요?

이상의 예시처럼 입력받은 데이터를 그대로 반환해야하는 함수가 있다고 가정해보겠습니다. string이 들어오면 string을, number가 들어오면 number를 반환해야합니다.

단순한 방식을 생각해보면 아래처럼 작성할 수 있습니다.

function logString<string>(content: string): string{
	console.log(content);
	return content;
}

function logNumber<number>(content: number): number{
	console.log(content);
	return content;
}

두 함수는 매우 비슷하게, 아니 js라면 그냥 이름만 다른 함수입니다. 동일한 기능을 하는 함수를 타입 때문에 두 번 정의하는 것은 누가봐도 비효율적입니다.

호출 시점에 타입이 정의되는 제네릭으로 함수 중복 문제를 해결할 수 있습니다.

3-1. 엥 유니온 쓰면 안되나요?

가능은 하쥬. 한 번 보시쥬.

전달받을 데이터가 무조건 string 또는 number라고 가정해보겠습니다. 아래와 같이 작성하면 argument의 타입은 string 또는 number이기만 하면 됩니다.

function logContent(content: string | number) {
  console.log(content);
  return content;
}

logContent('soryeongk');
logContent(25);

3-2. 엥 그럼 왜 굳이 제네릭?

지금처럼 단순한 작업말고 한가지를 더 넣어보겠습니다.

function logContent(content: string | number) {
  console.log(content);
  return content;
}

const name = logContent('soryeongk');
console.log(typeof name); // 결과는?
name.length // 결과는?

정답
console.log에는 string이 찍히고, name.length는 아래와 같은 error가 납니다.

왜?
분명 name을 string으로 인식했지만, 정작 string에서 제공하는 length는 사용할 수 없습니다. 이는 logContent의 인자의 타입은 string | number이기 때문에 본 타입에는 length가 없다고 인식합니다..

3-3. 유니온 대신 제네릭 쓰기

function logContent<T>(content: T): T {
  console.log(content);
  return content;
}

const name = logContent('soryeongk');
console.log(name.length) // 결과는?

정답
여전히 error가 납니다. 인자로 넘어온 string의 prototype을 사용하기 위해서는 제네릭을 사용하더라도 해당 인자가 무슨 타입인지를 지정해줘야합니다. 아래와 같이 사용하실 수 있습니다.

  function logContent<T>(content: T): T {
    console.log(content);
    return content;
  }

  const name = logContent<string>('soryeongk');
  console.log(name.length) // 9

4. 조금 더 활용해보자!

제가 속한 스터디(스파르타스: SparTaS)의 멤버들의 정보를 저장해보려합니다.
스터디원은 령이, 졍이, 언이, 수야, 희야, 함이입니다. 령이, 졍이, 언이, 수야는 97년생, 희야는 98년생, 함이는 빠른 98년생입니다.

interface SparTaS {
  name: string;
  born: number;
  isAbsent: boolean;
}

const ryeongE: SparTaS = { name: '령이', born: 97, isAbsent: false };
const jeongE: SparTaS = { name: '졍이', born: 97, isAbsent: false };
const eonE: SparTaS = { name: '언이', born: 97, isAbsent: false };
const heeYa: SparTaS = { name: '희야', born: 98, isAbsent: false };
// born: "빠른 98"
const suYa: SparTaS = { name: '수야', born: 97, isAbsent: true };

‘함이’를 입력하려고 하는데, 나이가 문제입니다. 함이는 25살이라고 박박 우기지만 사실 범띠이기 때문이죠.. 그래서 함이는 빠른 98이라고 입력하고 싶은데, 나머지는 number로 입력되어있어서 string을 넣을 수가 없네요..

4-1. 해결 방법을 생각해보자!

이 때 사용가능한 방법은 아래와 같습니다.

  1. 함이를 위한 타입을 따로 지정한다.
  2. 함이를 SparTaS에서 제외한다.
  3. born 부분을 유니언으로 사용한다.
  4. 인터페이스에 제네릭을 가미한다.

1번은 너무 비효율적이고, 2번은 간단하지만 매몰차네요,,

3번은 위에서 살펴본 것처럼 number 타입에서 제공하는 프로토타입을 사용할 수 없게 될 수도 있을 것 같아 좋아보이지 않아요.

그래서 4번의 방법으로 구현해볼게요!

4-2. 인터페이스에 제네릭 가미하기

일단 함이를 추가해봅니다. 역시 에러가 나네요..!

SparTaS의 born에만 제네릭을 사용하여 해결해볼까요?!

    interface SparTaS<T> {
      name: string;
      born: T;
      isAbsent: boolean;
    }
    
    const ryeongE: SparTaS<number> = { name: '령이', born: 97, isAbsent: false };
    const jeongE: SparTaS<number> = { name: '졍이', born: 97, isAbsent: false };
    const eonE: SparTaS<number> = { name: '언이', born: 97, isAbsent: false };
    const heeYa: SparTaS<number> = { name: '희야', born: 97, isAbsent: false };
    const suYa: SparTaS<number> = { name: '수야', born: 97, isAbsent: false };
    const HamE: SparTaS<string> = { name: '함이', born: '빠른 98', isAbsent: false };

제네릭을 사용하니, 함이를 위한 새로운 타입의 정보도 알맞게 넣을 수 있고, 추후 number 나 string 본연의 기능도 사용할 수 있게 되었어요! 우와!

5. 제네릭의 타입 제한

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

logTextLength('hi');

logTextLength가 정의된 순간에는 T에 어떤 타입이 들어올지 알 수 없습니다. 이 때, 함수 내에서 text의 length를 찍어보려고 하면 오류가 발생합니다.

그래서 T 안에 string, array 등 .length를 지원하는 타입이 있을 것임을 명시해줘야합니다.

5-1. 일단 방법을 알려드리죠

방법 1.

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

logTextLength('hi');

방법 2.

interface 활용

interface lengthType {
  length: number;
}

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

logTextLength('soryeongk');

text가 전달받을 타입 중에 number 타입을 가지는 length라는 프로퍼티가 있음을 알려주는 것입니다.

5-2. superType과 subType, extends는 확장인가 제한인가

부끄럽게도 superTypesubType이 정확한 명칭인지는 확인되지 않았습니다.. 슈퍼 뭐시기, 서브 뭐시기.. 아시는 분은 댓글 남겨주세요..!

interface Person {
	name: string;
  age: number;
}

interface Developer {
	name: string;
  age: number;
  isFE: boolean;
}

위 코드는 다음과 같이 작성될 수 있습니다.

interface Person {
	name: string;
  age: number;
}

interface Developer extends Person {
	isFE: boolean;
}

여기서 Developer는 Person의 subType입니다. Person은 Developer의 superType입니다.

Person은 타입이 두 개밖에 없고 Developer에는 3개나 있는데?!
그래도 Person이 super이고 Developer가 sub입니다.
더 많은 제한을 가지고 있는 친구가 sub가 됩니다.

예시를 볼까요?

아래 코드에서 오류가 뜨는 것은 몇 번일까요?

let person: Person;
let developer: Developer;

person = developer;  // 1번
developer = person;  // 2번

정답은 2번입니다 :) 이유를 잘 모르시겠다면 댓글 남겨주세요!

5-3. keyof 활용

keyof는 Object의 key들의 literal 값들을 가져옵니다.

interface Person {
	name: string;
	age: number;
}

type test = keyof Person; // ('name', 'age')

이것을 제네릭 제한에 활용해보게씁니당

interface Person {
    name: string;
    age: number;
}

function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

let person: Person = {
    name: 'soryeongk',
    age: 25
};

let myString: string = pluck(person, 'name'); 
let myNumber: number = pluck(person, 'age'); 
console.log(myString); // 결과는?
console.log(myNumber); // 결과는?

keyof를 통해 받아올 key값을 한정하는 데 사용됩니다! :)

좋은 웹페이지 즐겨찾기