Generic문법은 함수 혹은 클래스 내부에서 사용할 타입을 외부에서 지정하는것을 의미한다
제네릭 타입은 TypeScript뿐만 아니라 Java, Go등 다양한 언어에 있는 특징이다.
예를 들어 두개의 객체를 Object.assign을 통해 합친 후 반환하는 함수가 있다고 가정하자.JavaScript의 Object클래스는 a와 b라는 서로 다른 객체가 있으면 b로부터 a와 동일한 키를 가지고 있는 값들과 a에 존재하지 않는 b의 프로퍼티쌍을 복사하는 메소드이다. b가 객체타입이 아니더라도 별도의 오류는 나지 않는다. 여기서 중요한 점은 a와 b가 모두 객체타입이라는 것이다.
일반적인 TypeScript 함수를 작성해본다. 그리고 이 함수를 호출한 후 age
라는 속성에 접근하도록 작성해본다.
function merge(objA: object, objB: object) {
return Object.assign(objA, objB);
}
const value = merge({ name: "hoplin" }, { age: 25 });
value.age; // 'object' 형식에 'age' 속성이 없습니다.ts(2339)
일반적으로 위 속성이 JavaScript에서 잘못된 문법이 아니라는것을 알지만, TypeScript는 object를 반환한다고 추론하고 있다.
이를 방지하기 사용할 수 있는것이 제네릭타입이다. 제네릭 타입을 활용해서 merge함수의 매개변수인 objA, objB의 타입을 외부에서 결정하도록 한다. 제네릭을 추가하는 방법은 아래와 같으며, 제네릭 타입은 여러개가 선언될 수 있다.
function name<T,U....>(param1:T,param2:U,....){
}
위 함수를 제네릭을 활용해 표현해본다. objA와 objB를 각각 다른 타입을 갖도록 제네릭을 작성한다.
function merge<T, U>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
merge를 호출하는 부분이 아래와 같이 각각의 객체들의 타입으로 인식되는것을 볼 수 있다.
여기서 문제점이 하나 더 생긴다. 제네릭은 외부에서 타입을 결정해 주는것이라고 하였다. 그렇기 때문에 사용자가 객체가 아닌 다른 타입의 값을 넣으면 어떻게 될까?
타입스크립트는 별도의 타입 오류를 잡지 못하고 호출시 제네릭 타입은 T를 number로, U를 리터럴타입으로 결정하는것을 볼 수 있다. 이런 경우 의도치 않은 결과가 나올 수 있기 때문에 위험하다
또 다른 예시로 배열을 입력받아 배열과,배열에 몇개의 원소가 들었는지가 저장된 튜플타입을 반환한다고 가정하자. 그러면 아래와 같이 T를 Array 타입으로 범위를 제한하여 표현할 수 있다.
function countAndDescribe<T>(elements: Array<T>): [Array<T>, string] {
let descriptionText = "Got no values";
if (elements.length) {
descriptionText = `Got ${elements.length} elements`;
}
return [elements, descriptionText];
}
countAndDescribe([1, 2, 3, 4]);
Java에서 제네릭문법은, extend, super키워드를 통해 타입의 하한, 상한을 제한한다. TypeScript는 아쉽게도 super키워드는 없지만 extend 키워드는 존재한다.
위 예시에서 결국 매개변수들을 object 타입으로 제한해야하는 조건이 있다. 제네릭 타입에 extends키워드를 통해 최소한 object 타입이 만족되는 타입들만 올 수 있도록 제한해보자. extends 키워드로 제한하는 방법은 아래와 같다.
<T extends object, U extends number, .... >
function merge<T extends object, U extends object>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
위처럼 함수를 고치고 호출부를 살펴보면, number타입이므로 오류가 나는것을 볼 수 있다.
그리고 다시 객체 타입으로 변경하면, 해결되는것을 볼 수 있다.
간단하게 객체와 객체의 key값을 받아서 객체로부터 key값에 해당하는 value를 반환하는 함수가 있다고 가정해본다. Generic 타입 제한을 사용해 함수를 작성해 보면 아래와 같이 작성할 수 있다.
function extractAndConvert<T extends object, U extends string>(obj: T, key: U) {
return obj[key];
}
하지만 위 코드는 오류가 난다.
위 코드의 key라는 값이 객체 T의 key일것이라는 보장이 없기 때문이다. 이런경우 keyof
키워드를 사용하면 된다. keyof
키워드는 특정 객체의 Key값들을 유니온 타입으로 타입을 추론하게끔 되어있다.
<T extends object, U extends keyof T>
함수를 아래와 같이 고치면 타입스크립트는 사용자가 첫번째 매개변수에 건넨 객체의 키값들로만 값을 전달할 수 있도록 자동완성을 제공한다.
function extractAndConvert<T extends object, U extends keyof T>(
obj: T,
key: U
) {
return obj[key];
}