Generic

프로그램을 작성할 때 가장 중요한 사항 중 하나는 재사용 가능한 구성 요소를 만드는 것입니다. 이를 잘 지킨다면 프로그램이 장기적으로 유연할 뿐만 아니라 확장 가능(scalable)하다는 것을 보장하기 때문입니다.

TypeScript에서 제네릭(Generic)으로 재사용 가능한 구성 요소를 만들 수 있습니다. 제네릭은 구성 요소가 한 가지 데이터 타입으로 제한되지 않고 모든 데이터 타입에서 작동할 수 있도록 합니다. 따라서 구성 요소는 다양한 데이터 타입과 함께 호출되거나 사용될 수 있습니다. TypeScript의 제네릭은 C#의 제네릭과 거의 유사합니다.

다음의 예제를 통해 왜 제네릭이 필요한 지 알아봅시다.

function getArray(items : any[] ) : any[] {
    return new Array().concat(items);
}

let myNumArr = getArray([100, 200, 300]);
let myStrArr = getArray(["Hello", "World"]);

myNumArr.push(400); // OK
myStrArr.push("Hello TypeScript"); // OK

myNumArr.push("Hi"); // OK
myStrArr.push(500); // OK

console.log(myNumArr); // [100, 200, 300, 400, "Hi"]
console.log(myStrArr); // ["Hello", "World", "Hello TypeScript", 500]

위 예제에서 getArray()함수는 any 타입의 배열을 인자로 받아 새롭게 생성한 배열과 합친 것을 반환합니다. 이는 any 타입을 허용하고 있기 때문에 특정한 에러없이 문자열과 숫자가 혼용된 배열을 만들 수 있습니다.

만약 숫자 배열에 숫자 타입만 추가하거나, 문자열 배열에 문자열만 추가하고 싶다면 어떻게 구현해야 할까요?

이를 해결하기 위해 TypeScript는 제네릭을 도입했습니다. 제네릭은 타입 변수 <T>를 사용합니다. 이는 타입을 나타내는 특수한 종류의 변수입니다. 타입 변수는 사용자가 제공한 타입을 기억하고 해당하는 타입만을 받을 수 있도록 작동합니다. 이를 타입 정보 보존이라고 합니다.

위의 함수는 아래와 같이 제네릭 함수로 다시 작성할 수 있습니다.

function getArray<T>(items : T[] ) : T[] {
    return new Array<T>().concat(items);
}

let myNumArr = getArray<number>([100, 200, 300]);
let myStrArr = getArray<string>(["Hello", "World"]);

myNumArr.push(400); // OK
myStrArr.push("Hello TypeScript"); // OK

myNumArr.push("Hi"); // Compiler Error
myStrArr.push(500); // Compiler Error

위 예제에서 타입 변수 T는 꺾쇠<> 괄호 안의 함수로 지정합니다. T는 매개변수나 반환 값의 타입을 특정할 수 있습니다. 이는 함수 호출시에 지정되는 데이터 타입이 매개변수나 반환 값의 데이터 타입과 동일함을 의미합니다.

getArray<number>([100, 200, 300])과 같이 호출하는 경우 Tnumber로 대체됩니다. 따라서 매개변수와 반환 값 모두 숫자 배열이 됩니다. 만약 문자열을 해당 배열의 추가할 때에는 컴파일러에서 에러가 발생합니다.

다중 타입 변수

다음과 같이 여러가지 이름을 활용하여 타입 변수를 지정할 수 있습니다.

function displayType<T, U>(id:T, name:U): void {
  console.log(typeof(id) + ", " + typeof(name));
}

displayType<number, string>(1, "Steve"); // number, string

제네릭 타입은 다음과 같이 논 제네릭 타입과도 혼용하여 사용할 수 있습니다.

function displayType<T>(id:T, name:string): void {
  console.log(typeof(id) + ", " + typeof(name));
}

displayType<number>(1, "Steve"); // number, string

제네릭 인터페이스

제네릭 타입은 인터페이스와 함께 사용할 수 있습니다. 다음의 예제에서 IProcessor는 제네릭 인터페이스로 result 프로퍼티와 process 메소드의 매개변수와 반환 값 모두 제네릭 타입을 사용해야 합니다.

interface IProcessor<T>
{
    result:T;
    process(a: T, b: T) => T;
}