Lsiron

Type Script 의 Generic 이란? 본문

언어/Type Script

Type Script 의 Generic 이란?

Lsiron 2024. 7. 17. 11:00

Generic 이란?

TypeScript에서 제네릭(generic)은 타입을 변수처럼 다룰 수 있게 해주는 강력한 기능이다. 제네릭을 사용하면 함수, 클래스, 인터페이스 등을 다양한 타입에 대해 재사용할 수 있다.

 

제네릭은 타입 변수를 사용하여 정의된다. 타입 변수는 보통 T, U, V 등과 같은 이름으로 명명되며, 함수나 클래스, 인터페이스에 특정 타입이 아닌 다양한 타입을 사용할 수 있도록 한다.

 

제네릭을 사용하는 이유

  • 재사용성: 여러 타입에 대해 동일한 로직을 적용할 수 있다.
  • 타입 안전성: 런타임에 타입 오류가 발생하는 것을 방지할 수 있다.
  • 가독성: 코드의 의도를 명확히 하고, 타입에 대한 제약 조건을 명시할 수 있다.

제네릭 사용법

제네릭함수 사용법은 파라미터로 타입을 입력하는 함수 이기 때문에 아래와 같이 함수에 사용할 땐, 함수 우측 <> 안에 파라미터를 지정해 주고 사용할 땐 함수명 우측 <> 안에 타입을 입력하면 된다.

function iron<T>(x: T[]) :T {
  return x[0];
}

let a = iron<number>([1,2])
let b = iron<string>(['lsiron', 'siron'])

 

타입이 필요한 곳에는 어디든지 제네릭함수를 적용 할 수 있다. 위 예시의 경우 타입 파라미터를 T로 넣어주었다. 

 

그러면 이 함수를 사용할 때 함수명과 인자 사이에 있는 <> 에 타입만 입력 해 주면 함수의 타입을 즉석으로 정하여 사용할 수 있는 것이다. 

 

변수 a의 경우 number 타입을 모든 T에 넣어달라는 의미이고 변수 b의 경우 string 타입을 모든 T에 넣어달라는 의미이다.

 

참고로 위 변수처럼 number, string으로 타입 파라미터를 지정 해 주지 않아도 자동으로 타입을 유추한 뒤, 적용시켜준다.

function iron<T>(x: T[]) :T {
  return x[0];
}

let a = iron([1,2]) // number로 적용함
let b = iron(['lsiron', 'siron']) // string으로 적용함

 

그래도 명시적으로 타입 파라미터를 표시 해 주는 것이 좋다.

 

타입 파라미터 제한

function iron<T>(x: T) :T {
  return x + 1;		// 에러 발생!!!
}

let a = iron<number>(100)

 

위 예시의 함수를 보자.

 

x + 1 을 return 하기 때문에 이는 수학연산을 사용하고 있다. 이 수학연산은 number 타입에만 가능하다. 

 

즉, 에러가 발생하는 이유는 T 파라미터를 넣어줄 때, number로 확정 지어주지 않았기 때문이다. 이로 인해 함수 자체에서 에러가 발생한 것이다.

 

이러한 에러가 발생하지 않도록 하기 위해, T 파라미터 에는 number 타입만 들어올 수 있게 Narrowing을 해 주어야 한다. 

 

이때 제네릭 함수에서 타입파라미터에 제한을 두는 방법은 바로 extends를 사용하는 것이다.

function iron<T extends number>(x: T) :T {
  return x + 1;		
}

let a = iron<number>(100)

console.log(a) // 101 출력

 

이제 extends로 인해 T 파라미터는 number 타입으로 제한이 된다. T 파라미터가 number 속성을 가지고 있는지 체크 하는 것이다. 

( interface와 class에서 사용하던 extends 처럼 복사의 개념이 아니라 체크의 개념이다. )

 

참고로 이 extends는 응용하여 사용 될 수 있다.

 

extends 우측에는 number, string 처럼 반드시 정해진 타입만 들어 오는 것이 아니라 interface, type alias 등을 넣어 줄 수 있다. 

interface LengthCheck {
    length : number
}

function iron<T extends LengthCheck>(x: T) :T {
  return x.length;		
}

let a = iron<string>('100')

console.log(a) // 3 출력

 

string이나 array 같은 경우 .length를 붙일 수 있기 때문에 에러가 발생하지 않는다.

 

함수에서의 제네릭

다음은 제네릭을 사용하여 여러 타입에 대해 동작할 수 있는 함수를 정의하는 예시이다.

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

let number = identity<number>(42); // T는 number로 추론됨
let text = identity<string>("Hello, TypeScript!"); // T는 string으로 추론됨

 

이 예시에서 identity 함수는 인수의 타입과 동일한 타입을 반환한다. T는 타입 변수로, 호출 시점에 타입을 지정할 수 있다.

 

클래스에서의 제네릭

제네릭을 사용하여 다양한 타입의 데이터를 처리할 수 있는 클래스를 정의할 수도 있다.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zeroValue: T, add: (x: T, y: T) => T) {
        this.zeroValue = zeroValue;
        this.add = add;
    }
}

let myNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myNumber.add(myNumber.zeroValue, 5)); // 5

 

여기서 GenericNumber 클래스는 숫자 타입뿐만 아니라 다른 타입에도 사용할 수 있다.

 

인터페이스에서의 제네릭

인터페이스에서도 제네릭을 사용할 수 있다.

interface Pair<T, U> {
    first: T;
    second: U;
}

let numberStringPair: Pair<number, string> = {
    first: 1,
    second: "one"
};

console.log(numberStringPair);

 

Pair 인터페이스는 두 개의 타입 T와 U를 받아 각각 first와 second에 할당한다.

 

참조 : 코딩애플