제네릭을 사용하면 함수가 파라미터를 받듯이 타입을 파라미터화 해서 여러가지 타입을 받아줄 수 있습니다.
즉 여러번 재사용이 가능한걸 만들때 사용할 수 있겠죠?
아래 코드는 인자를 하나 넘겨 받아 반환해주는 함수입니다.
function logText(text: string): string {
return text;
}
tring
으로 지정되어 있지만 만약 여러 가지 타입을 허용하고 싶다면 아래와 같이 any
를 사용할 수 있습니다.
function logText(text: any): any {
return text;
}
이렇게 타입을 바꾼다고 해서 함수의 동작에 문제가 생기진 않습니다.
하지만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없습니다.
왜냐하면 any라는 타입은 타입 검사를 하지 않기 때문입니다.
제네릭은 위와 같은 상황에서의 문제점을 근본적으로 해결할 수 있습니다.
먼저 Promise를 만들어 2번째 인자로 넘겨준 시간만큼 뒤에 첫번째로 넘겨준 X를 resolve하는 함수를 만들어줍니다.
function createPromise(x, timeoute: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(x);
}, timeoute);
});
}
이렇게 하면 다음과 같이 1초뒤에 1을 콘솔창에 찍어줄 수 있겠죠?
하지만 지금 시점에서 x의 인자로 무엇이 들어올지는 알수가 없습니다.
createPromise(1, 1000).then((v) => console.log(v));
그런데!! 만약 위 createPromise함수에서 매개변수 x의 타입을 number로 지정해버리면 인자로 숫자밖에 넣을 수 없습니다.
이런 경우에는 제네릭을 이용해 타입변수를 사용해 줄 수 있습니다.
function createPromise<T>(x: T, timeoute: number) {
return new Promise((resolve: (v: T) => void, reject) => {
setTimeout(() => {
resolve(x);
}, timeoute);
});
}
<T>
는 타입의 파라미터이기 때문에 해당 함수 어디서든 사용해 줄 수 있습니다.
그렇다면 <T>
를 어떻게 어디에서 정의해 주어야 할까요?
createPromise<string>("string", 1000)
.then((v) => console.log());
우선 위와 같은 방법으로 직접 타입을 지정해 줄 수 있습니다.
createPromise("string", 1000)
.then((v) => console.log());
이렇게만 사용해도 첫번째 인자의 타입이 T이기 때문에 T는 문자타입이 지정되겠죠?
또한 Promise함수를 생성할때도 다음과 같이 그냥 명시해 줄 수 있습니다 .
function createPromise<T>(x: T, timeoute: number) {
return new Promise<T>((resolve, reject) => {
setTimeout(() => {
resolve(x);
}, timeoute);
});
}
함수의 파라미터를 정의할때 여러개를 정의할 수 있듯이, 제네릭에서도 여러개의 타입을 넣어 줄 수 있습니다.
간단하게 튜플을 만들어주는 함수를 만들어 볼까요?
function createTuple<T, U>(v: T, v2: U): [T, U] {
return [v, v2];
}
const t1 = createTuple(1, 2);
이렇게 된다면 [숫자,숫자]
형의 tuple이 생성이 될 것입니다.
물론 아래와 같이 3개짜리 튜플도 생성이 가능합니다!
function createTuple<T, U, D>(v: T, v2: U, v3: D): [T, U, D] {
return [v, v2, v3];
}
const t1 = createTuple(1, 2, 3);
제네릭은 클래스와 인터페이스에서도 사용할수 있습니다!
먼저 interface를 하나 만들어주세요.
interface DB<T> {
add(v: T): void;
get(): T;
}
그리고 interface를 참조하는 class D를 만들어줍니다.
class D<T> implements DB<T> {
add(v: T): void {
throw new Error("Method not implemented");
}
get(): T {
throw new Error("Method not implemented");
}
}
class
에서interface
를implements
할때는 꼭 로 타입을 지정한 것처럼 둘다 지정 해 주어야만 합니다.
즉 인터페이스에서도 아직 정의되지는 않았지만 타입을 가지고 있는것으로 정의할 수 있고 그걸 class
에서 implements
를 할때 실질적으로 구현할때도 그 제네릭타입을 이용해 코딩할수 있습니다.
흔히 사용하는 삼항연산자를 통해 조건부로 타입을 지정해 줄 수도 있습니다.
예를들어 야채와 고기, 그리고 카트라는 인터페이스를 하나 만들어주세요.
interface Vegitable {
v: string;
}
interface Meat {
m: string;
}
interface Cart<T> {
getItem(): T extends Vegitable ? Vegitable : Meat;
}
getItem
의 반환타입 T
는 반약 타입이 야채가 들어왔을 경우에는 야채이지만, 나머지 모든 경우에는 고기로 보겠다 라는 의미를 가지게 됩니다.
그럼 제네릭으로 야채가 아닌 문자열을 지정하면 어떻게 될까요?
const cart1: Cart<string> = {
getItem() {
return {m: "고기"};
},
};
위와같이 return값
이 조건부 타입에 의해 고기가 되어서 m
이 있는 객체 즉 Meat
인터페이스 타입이 반환되어야 합니다.
반대로 야채를 넣어주아래와 같은 에러가 출력됩니다.
조건부 타입에 의해 반환타입은 야채이니 {v:’~~’} 를 반환해라 라는 의미겠죠?
조건부 타입을 이용해 특정한 메소드에서 반환되는 타입을 제네릭에 의해 다르게 결정되도록 만들 수 있는 것입니다.
제네릭을 사용하면 하나의 타입이 아닌 여러타입을 사용할 수 있게, 즉, 고정된 타입이 아니라 유동적으로 타입을 변경해줄수 있으며, 동시에 타입세이프한 코드를 작성할 수 있도록 도와주게 됩니다.