Front-End(Web)/Typescript

[Typescript] 제네릭(Generic)

ttaeng_99 2021. 2. 23. 19:26
반응형

우선, Generic 이라는 말부터 생소했다. '일반적인' 이라는 뜻으로, 정적타입 언어에서 이를 많이 지원한다고 한다.

클래스나 함수에서 사용할 타입을 결정하면서, 소위 '일을 어떻게 할 것인지?' 를 결정하는 문법이라고 한다. 자세히 공부해보았다!


💙 Generic 이란?

'일반적인' 이라는 언어 뜻 그대로, 재활용성이 높은 컴포넌트를 구축하기 위한 문법이다.

정적언어인 C#과 Java에서는 재사용 가능한 컴포넌트를 만드는 주요 도구로 제네릭이 통용된다.

 

- Generic 적용

// 단일 타입
function identity(arg: number): number {
    return arg;
}

// any 타입
function identity(arg: any): any {
    return arg;
}

아래와 같은 포멧의 컴포넌트(identity)를 가정하자. 제네릭이 없다면, 이 함수를 기술할 때 특정 타입을 지정해야한다.

혹은, 이를 any를 지정해서 모든 타입을 적용할 수 있다. 하지만, 이는 타입스크립트가 지양하는 방법이며 반환타입의 정보를 얻을 수 없다.

 

function identity<T>(arg: T): T {
    return arg;
}

위와 같은 문제로, 우리는 반환값의 타입을 확인하기 위해 매개변수의 타입을 캡쳐해야한다. 여기서, 타입 매개변수 문법을 사용한다.

<변수명>으로 적용한다. 우리가 T에 원하는 타입을 넣어주면, 매개변수(arg)와 반환값의 타입이 해당 타입으로 연동되는 것이다.

또한, any와는 다르게 매개변수와 반환값의 타입이 T 설정값임을 인지하기 때문에, 정확도 면에서도 높다.

 

// 1) 타입인수 전달
let output = identity<string>("myString");

// 2) 타입인수 추론
let output = identity("myString");

제네릭 문법에서 타입을 지정하는 방법은 2가지이다.

  1. 타입인수 전달 : 우리가 만든 <T> 안에 타입을 할당하는 것이다. 함수명과 ()괄호 사이에 적으며, <>로 감싼다.
  2. 타입인수 추론 : 별도 입력이 없으면, 입력한 매개변수 타입에 따라 추론한다. 간결하나, 복잡한 로직에선 불리한 방법이다.

💙 Generic 다양한 문법

- 변수/함수와 Generic

function getItemArray<T>(arr:T[], index:number):T {
  return arr[index];
}

function pushItemArray<T>(arr:T[], item:T):void {
  arr.push(item);
}

const materials = ['어니언'];

getItemArray(materials, 0);                 // '어니언'
pushItemArray<string>(materials, '사워크림'); // ['어니언', '사워크림']

다음은, 함수에 제네릭을 적용한 예제이다. 배열의 경우, any가 아닌 제네릭(다른 변수인 U 등)을 적용하면 [index] 구문에서 오류가 생긴다.

그렇기에, 이처럼 T[] 제네릭을 적용해서, 문자열로 이뤄진 배열이라는 것을 지정해주는 것이다.

 

pushItemArray() 함수는, <string> 제네릭을 붙여 T변수 자리에 문자열 타입을 지정해준다.

getItemArray() 함수 역시 제네릭을 붙여야하지만, 위처럼만 작성했을 때 materials 배열 내 요소가 문자열인것을 통해 추론을 한다.

 

function identity<T>(arg: T): T {
  return arg;
}

// 1) 다양한 변수명
let myIdentity: <U>(arg: U) => U = identity;

// 2) 객체 리터럴 타입 적용
let myIdentity: { <T>(arg: T): T } = identity;

// 3) 인터페이스 적용(제네릭 참조)
interface GenericIdentityFn {
  <T>(arg: T): T;
}

let myIdentity: GenericIdentityFn = identity;

// 4) 인터페이스 적용(제네릭 지정)
interface GenericIdentityFn<T> {
  (arg: T): T;
}

let myIdentity: GenericIdentityFn<number> = identity;

위 예제는, 제네릭을 변수/함수에 적용할 수 있는 다양한 경우의 문법이다.

  1. 다양한 변수명 : 타입 매개변수명은 <T> 가 아닌 다양한 값을 사용할 수 있다. 알파벳 순으로 U부터 시작
  2. 객체 리터럴 타입 : 객체(Object)의 시그니처 타입을 제네릭으로 작성한 경우이다.
  3. 인터페이스 적용 : 인터페이스에도 시그니처 타입을 제네릭으로 작성한다. 3)은 다른 함수에서 참조, 4)는 직접 지정하는 방법

 

- 클래스와 Generic

class Model<T> {
  
  private _data:T[] = [];
  
  constructor(data:T[]=[]) {
    this._data = data;
  }
  
  get data():T[] { 
    return this._data; 
  }
  
  add(item:T):void { 
    this._data.push(item); 
  }
  
  remove(index:number):void { 
    this._data.splice(index, 1); 
  }
  
  item(index:number):T { 
    return this._data[index]; 
  }
  
  clear():void { 
    this._data = []; 
  }
}

const stringModel = new Model<string>();

stringModel.add('흔들의자');

클래스도 마찬가지로 제네릭을 적용할 수 있다. 문법도 똑같이, 클래스명 옆에 타입 매개변수를 붙여준다.

이후, 인스턴스를 생성할 때 타입을 지정하면 된다. 제네릭은 인스턴스 측면이므로, 정적(Static) 멤버는 타입 매개변수를 적용할 수 없다.

만약, 이외의 타입을 적용하려면 Type Assertion을 통해 우회하면 되나, 꼭 필요한 경우가 아니면 지양해야하는 방법이다.

stringModel.add(2018 as any);
stringModel.add(<any>2018);

 

- 멀티 타입 설정

function pushPairItem<T,M>(arr:pairArray, item:[T,M]):pairArray {
  arr.push(item);
  return arr;
}

pushPairItem<boolean, string>(data, [false, 'false']);
pushPairItem<number, string>(data, [2019, '이천십구년']);

이처럼, <타입변수1, 타입변수2> 형식으로 타입 매개변수를 복수로 적용할 수 있다.

 

- 타입 변수 상속

제네릭 타입 매개변수는 기존의 타입 변수를 상속할 수도 있다.

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

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // 성공
getProperty(x, "m"); // 오류: 인수의 타입 'm' 은 'a' | 'b' | 'c' | 'd'에 해당되지 않음.

타입 매개변수 K를 보자. T라는 객체 매개변수의 상속을 통해 이것의 key를 타입으로 지정한 것이다. 


제네릭까지 공부하면서, 타입스크립트에는 컴포넌트 재활용을 위한 다양한 문법들을 지원해준다는 생각이 들었다.

(Type Aliase, Interface 등등...)

 

그렇다보니, 문법 간에 중첩되는 모호한 부분도 발생한다. (클래스와 인터페이스, 타입 엘리어스와 인터페이스)

타입스크립트의 중점은, 재활용을 위한 적절한 설계를 고려하는 것, 그리고 이 목적에 맞는 문법을 선정하는 것이 중요할 것 같다.

 

[출처]

- Typescript 핸드북 : typescript-kr.github.io/pages/generics.html  

- Typescript 가이드북 : yamoo9.gitbook.io/typescript/generics/type-inheritance

반응형