2023년 3월 16일 목요일

TypeScript 제네릭 (Generics)

 

제네릭 (Generics)

제네릭(Generics)은 하나의 데이타 타입이 아닌 여러 데이타 타입에 대해 클래스/인터페이스 혹은 함수가 동일하게 동작할 수 있게 해주는 기능이다. TypeScript의 제네릭은 C#의 제네릭과 거의 유사하다. 제네릭에는 함수에 제네릭 타입을 적용한 제네릭 함수(generic function), 인터페이스에 제네릭 타입을 넣은 제네릭 인터페이스, 클래스에 제네릭을 적용한 제네릭 클래스 등이 있다.

제네릭 함수

제네릭 함수는 여러 데이타 타입을 갖는 함수를 각각 정의하는 대신 하나의 제네릭 함수를 사용하면서 타입 파라미터로 어떤 데이타 타입을 갖는지를 표시하는 함수이다. 먼저 다음과 같은 여러 데이타 타입을 갖는 함수들이 있다고 가정해 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function displayNumbers(data: number[]): void {
    for (const elem of data) {
        console.log(elem);
    }
}
 
function displayStrings(data: string[]): void {
    for (const elem of data) {
        console.log(elem);
    }
}
 
function displayBooleans(data: boolean[]): void {
    for (const elem of data) {
        console.log(elem);
    }
}

위 함수들은 data 라는 하나의 파라미터를 받아들이는데, data 의 데이타 타입이 다르기 때문에 하나의 함수로 만들지 못하고 여러 비슷한 이름의 함수들을 만들었다. 이들 함수에서 차이점은 입력 데이타 타입뿐이고, 나머지 코드는 모두 공통적으로 사용할 수 있다. 이러한 경우에 제네릭을 사용하여 아래와 같이 하나의 제네릭 함수를 정의할 수 있다. 여기서 <T> 는 타입 파라미터 (type parameter)라고 불리우는 것으로 제네릭을 사용할 때 외부에서 정의하는 타입을 의미한다. 즉, 이 타입 파라미터에 number를 정의하면 위의 displayNumbers() 함수와 같은 기능을 하는 것이고, string을 정의하면 displayStrings()와 같은 기능을 하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function display<T>(data: T[]): void {
    console.log(data.length);
    for (const elem of data) {
        console.log(elem);
    }
}
 
display<number>([1, 2, 3]);
display<string>(["Tom", "Tim", "Matt"]);
display<boolean>([true, false]);   
 
// 타입체킹 오류:
// Type 'string' is not assignable to type 'number'
display<number>([1, "2", 3]); 

제네릭을 사용할 때, 타입 파라미터는 하나 이상 복수개를 정의할 수 있으며, 이 타입 파라미터는 입력 파라미터, 리턴 타입, 함수 본문 등 어느 곳에서도 사용될 수 있다. 특별한 제약(type contraint)을 가하지 않는 한, 타입 파라미터는 모든 타입을 받아들일 수 있으며, 따라서 모든 타입에 적용될 수 있는 제네릭 함수을 작성해야 한다. 제네릭은 모든 타입을 받아들인다는 점에서 입력파라미터 혹은 리턴타입에 any 타입을 쓰는 것과 비슷한 점이 있지만, 제네릭은 사용시 타입파라미터를 구체적으로 정의하기 때문에 구체적인 입력 타입, 리턴타입을 정의한 것과 같이 타입 체킹을 하게 된다는 장점이 있다.

제네릭 클래스/인터페이스

제네릭은 제네릭 함수와 마찬가지로 인터페이스나 클래스에 적용될 수 있다. 아래 예제는 간단한 제네릭 인터페이스와 이를 구현하는 제네릭 클래스를 예시한 것이다. 아래 코드에서 보듯이, 인터페이스와 클래스에서 각각 타입파라미터 T 를 받아들이고 있고, 이를 클래스와 인터페이스 내에서 사용하고 있다. 타입파라미터 T는 속성과 메서드, 입력파라미터와 리턴타입 등에 모두 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 제네릭 인터페이스
interface IExport<T> {
    export(): T;
}
 
// 제네릭 클래스
class MyData<T> implements IExport<T> {
    private _data: T;
 
    constructor(data: T) {
        this._data = data;
    }
 
    export(): T {
        let cloned  = Object.assign({}, this._data);
        return cloned;
    }
}
 
class Person { id: number; name: string; }
let p: Person = {id: 1, name:"Tim" };
 
let o = new MyData<Person>(p);
let cp: Person = o.export();
console.log(cp.name);
제네릭 타입 제약 (Generic Type Contraint)

제네릭의 타입 파라미터는 기본적으로 모든 타입을 받아들일 수 있다. 만약 모든 타입을 받아들이지 말고, 제약된 타입들만 받아들이고자 한다면, 이러한 제약을 타입 파라미터에 지정할 수 있는데, 이를 Generic Type Contraint라 부른다.

아래 예제는 Mailer 제네릭 클래스의 타입파라미터가 Person 타입 (혹은 그 서브클래스)이어야 한다는 제약을 지정한 예이다. 즉, 타입 T 가 Person 클래스나 그 서브클래스이어야 한다는 것을 지정하기 위해, T extends Person 과 같이 extends 를 사용하여 제약 타입을 지정하였다. 만약 제약타입이 인터페이스인 경우에도 T extends IntefaceName 과 같이 extends 를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person {
    name: string;
    email: string;
}
 
class Employee extends Person {
    dept: string;
    jobtitle: string;
}
 
class Mailer<T extends Person> {
    private sender = "sales@test.com";
 
    sendMail(receiver: T) {
        console.log("Send from " + this.sender
                    +" to " + receiver.email);
    }
}
 
let mailer = new Mailer();
let p = new Person();
p.email = "tom@test.com";
mailer.sendMail(p);
 
let emp = new Employee();
emp.email = "jack@test.com";
emp.dept = "IT";
mailer.sendMail(emp);

댓글 없음:

댓글 쓰기