[TS] 유연한 타입 확장의 도구, 제네릭

chaevivi·2023년 10월 18일
0
post-thumbnail

타입을 유연하게 사용할 수 있도록 도와주는 제네릭



1. 제네릭이란?

제네릭은 타입을 미리 정의하지 않고 사용하는 시점에 원하는 타입을 정의해서 쓸 수 있는 문법입니다.

재사용 가능한 컴포넌트를 만들기 위해 타입스크립트에서 사용하는 도구 중 하나는 제네릭입니다. 즉, 단일 타입이 아닌 다양한 타입에서 작동하는 컴포넌트를 작성할 수 있습니다.
사용자는 제네릭을 통해 여러 타입의 컴포넌트나 자신만의 타입을 사용할 수 있습니다.



2. 제네릭의 기본 문법

function getText(text: any): any {
  return text;
}

위의 코드를 살펴보면,

  • getText 함수는 텍스트를 인수로 넣으면 그대로 반환하는 함수입니다.
  • any 타입을 사용하면 인수의 타입이 어떤 타입이든 상관 없다는 뜻입니다.
  • 만약 number 타입을 넘긴다면 결국 any 타입이 반환되어 number 타입을 반환할 수 없습니다.

  • 무엇을 반환하는지 표시하기 위해서는 타입 변수가 필요합니다.
function getText<Type>(text: Type): Type {
  return text;
}
  • Type은 사용자가 준 인수의 타입을 캡처하고, 인수와 반환 타입을 같은 타입을 사용할 수 있도록 해줍니다.
  • 이를 통해 타입 정보를 함수의 한쪽에서 다른 한쪽으로 운반할 수 있습니다.
  • 제네릭을 사용하면 getText 함수를 실행할 때 아무 타입이나 넘길 수 있습니다.
    getText<string>('hi');     // hi
    • 위의 코드는 getText 함수에 string 타입을 할당한 코드입니다.
    • 위의 코드로 getText 함수에서 Type 타입 변수가 선언된 곳에 모두 들어갑니다.
      // 이 함수는 아래처럼 동작합니다.
      			function getText<string>(text: string): string {
        return text;
      }
      	
      function getText(text: string): string {
        return text;
      }
  • 타입을 전달할 때는 명시적으로 작성하지 않고 타입 추론을 이용할 수도 있습니다.
    getText('hi');
    • 타입을 괄호(<>)에 담아 명시적으로 전달하지 않아도 값 'hi'의 타입을 추론하여 Type 타입 변수에 전달해줍니다.
    • 이 방식을 인수 추론이라고 합니다. 인수 추론은 코드를 간결하고 가독성 있게 작성할 수 있지만, 더 복잡한 예제에서 타입을 유추할 수 없는 경우에는 명시적인 타입 인수 전달이 필요할 수 있습니다.


3. 제네릭을 사용해야 하는 이유 (장점)

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

function getNumber(num: number): number {
  return num;
}

위의 코드를 살펴보면,

  • getText 함수는 문자열 타입의 텍스트를 인수로 받아 그대로 반환합니다.
  • getNumber 함수는 숫자 타입의 텍스트를 인수로 받아 그대로 반환합니다.
  • 두 함수는 타입만 다를 뿐 같은 동작을 합니다. 이는 코드가 중복되는 문제점을 발생시킵니다.

  • 이를 해결하기 위해 any 타입을 사용할 수도 있습니다.
function getText(text: any): any {
  return text;
}
  • any 타입을 사용하면 타입을 다양하게 받을 수 있고 코드의 중복을 제거할 수 있습니다.
  • 하지만 이렇게 any를 사용하면 타입스크립트의 장점인 에러 사전 방지가 무색해 질 수 있습니다.

  • 이는 제네릭을 사용하면 해결할 수 있습니다.
function getText<T>(text: T): T {
  return text;
}
  • 제네릭을 사용하면 타입을 다양하게 받을 수 있고, 코드의 중복을 제거할 수 있고, 타입스크립트의 장점을 모두 가져갈 수 있습니다.
getText<string>('hi');     // hi
getText<number>(100);     // 100


4. 인터페이스와 클래스에서 제네릭을 사용하는 방법


4.1. 인터페이스

interface Product {
  value: string;
  selected: boolean;
}

interface Stock {
  value: number;
  selected: boolean;
}

위의 코드를 살펴보면,

  • 상품과 재고를 나타내는 인터페이스 2개가 있습니다. 각각의 인터페이스는 value와 selected 속성을 가지고 있습니다.
  • 만약 value에 다른 데이터 타입을 갖는 다른 인터페이스가 필요하다면 새로운 인터페이스를 정의해야 합니다.
    • 하지만 모든 데이터를 일일이 정의한다면 타입 코드가 많아져서 관리가 어려워집니다.

  • 이 문제를 제네릭을 사용해 해결할 수 있습니다.
interface Shoppingmall<T> {
  value: T;
  selected: boolean;
}
  • 제네릭을 사용하면 타입별로 인터페이스를 다르게 사용할 수 있습니다.
// 인터페이스를 각각 작성
const product: Product;
const stock: Stock;
const address: Address;

// 하나의 제네릭 인터페이스 작성
const product: Shoppingmall<string>;
const stock: Shoppingmall<number>;
const address: Shoppingmall<{ city: stirng; zipCode: string }>;

4.2. 클래스

class GetNumber<T> {
  value: T;
  add: (x: T, y: T) => T;
}

let num = new GetNumber<number>();
num.value = 10;
num.add = function (x, y) {
  return x + y;
}

위의 코드를 살펴보면,

  • GetNumber 클래스에는valueadd 속성이 있습니다. valueadd 속성은 모두 넘겨 받은 타입을 사용합니다.
  • 클래스를 선언할 때 number 타입으로 선언하였기 때문에 valueadd 속성은 number 타입이 됩니다.
    - add는 함수이므로, x와 y 인자 그리고 반환값이 모두 number 타입으로 선언됩니다.


5. 제네릭의 타입 제약

  • 제네릭의 타입 제약은 제네릭으로 타입을 정의할 때 좀 더 정확한 타입을 정의할 수 있게 도와줍니다.
  • 다시 말해, 특정 타입들로만 동작하는 제네릭 함수를 만들 수 있습니다.

5.1. extends

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

getText<string>('hi');    // hi
getText<number>(100);    // 100
getText<boolean>(true);    // true

위의 코드를 살펴보면,

  • getText 함수는 제네릭을 이용하여 여러 타입을 받을 수 있는 함수입니다.
  • 만약 length 속성을 갖는 타입만 취급하겠다고 하면 어떻게 해야 할까요?\
    • length 속성을 갖는 타입은 string, array, object가 있습니다.

  • 이때는 extends 키워드를 사용하여 타입을 제약할 수 있습니다.
function getText<T extends { length: number }>(text: T): T {
  return text.length;
}

getText('hi');    // 2
getText([1, 2, 3]);    // 3
getText({ title: 'abc', length: 123 });    // 123
getText(10);    // Error
  • extends 키워드로 length 속성을 갖는 타입만 받도록 제약하였습니다.
  • length 속성을 가지는 string, array, object가 아닌 타입을 넘겨 주면 에러가 발생합니다.

5.2. keyof

type Developers = keyof { name: string; skill: string; }
// type Developers = "name" | "skill"

위의 코드를 살펴보면,

  • keyof 키워드는 특정 타입의 키 값을 추출해서 문자열 유니언 타입으로 변환해 줍니다.
  • Developers 타입 별칭에 객체의 키인 nameskill이 유니언 타입으로 선언되었습니다.
    • 다시 말해, Developers 타입 별칭은 name이나 skill 타입을 가집니다.

function printKeys<T extends keyof { name: string; skill: string; })>(value: T) {
  console.log(value);
}

printKeys('name');    // name
printKeys('skill');    // skill

printKeys('address');    // Error
printKeys(100);    // Error

위의 코드를 살펴보면,

  • 제네릭을 정의하는 부분에 extends 키워드와 keyof 키워드를 조합하여 name이나 skill 타입만을 받도록 제약하였습니다.
  • name이나 skill 문자열 이외의 문자열이나 타입을 넘기면 에러가 발생합니다.



참고
📖 쉽게 시작하는 타입스크립트
🔗https://www.typescriptlang.org/docs/handbook/2/generics.html#handbook-content

profile
직접 만드는 게 좋은 프론트엔드 개발자

0개의 댓글