타입스크립트를 배우면서 느낀 자바스크립트와 가장 다른 점은 타입스크립트의 이름에 있듯이 타입을 설정하는 것이다. 최대한 모든 변수에 타입을 설정하면서 바로바로 코드의 잘못된 점을 바로잡을 수 있다는 것은 큰 장점이라고 느꼈다.
하지만 모든 경우의 수를 생각하면서 변수를 설정한다는 것이 불편하고 머리가 아프다고 느껴지기도 했다. 특히, 따로 타입을 설정해야 할 정도로 까다로운 경우에는 무심코 any를 적게 됐다.
최근 읽고 있는 이펙티브 타입스크립트에서 any는 최대한 지양해야 한다고 말한 것처럼 any를 사용하는 것은 많은 문제를 야기한다. 따라서, any를 대체하는 방법 중 하나인 generic에 대해서 공부해보고자 한다.
generic은 데이터의 타입을 일반화한다(generalize)한다는 것을 뜻한다고 한다. 타입을 일반화한다는 말은 어떤 것을 의미할까?
타입을 일반화한다는 것은 타입에 구애받지 않고 코드를 짜게 만들어준다는 의미이다. 예를 들어, 우리가 자바스크립트에서 함수를 선언하고 그 안에 숫자를 넣든 문자를 넣든 상관하지 않듯이 사용할 수 있다는 의미이다.
하지만 자바스크립트와 동일한 방식은 아니다. generic은 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.
그렇다면 generic이 없다면 어떻게 될까?
우리가 generic이 없이 코드를 짜는 방식은 크게 두 가지이다.
const newFunc = (input: number): number => {
return input;
};
newFunc(3)
newFunc("cake") // error
newFunc([1, 2, 3]) //error
확실한 타입체크가 이뤄질 수 있겠지만 항상 number라는 타입을 받아야하므로 매개변수로 숫자를 넣을 수 있는 경우에만 사용할 수 있다.
const newFunc = (input: any): any => {
return input;
};
newFunc(3)
newFunc("cake")
newFunc([1, 2, 3])
코드의 재사용은 가능하지만 자료의 타입을 제한할 수 없을 뿐더러, 이 function을 통해 어떤 타입의 데이터가 리턴되는지 알 수 없다.
하지만 generic을 사용하면 코드를 여러번 재사용하면서도 확실한 타입체크가 가능하다.
매개변수 부분의 타입을 임의의 타입, Type으로 설정해둔 뒤 생성시점(활용시점)에 Type을 특정 타입으로 지정하는 방법이다.
const newFunc = <Type>(input: Type): Type => {
return input;
};
newFunc<number>(3)
newFunc<string>('cake')
newFunc<number[]>([1, 2, 3])
newFunc<string[]>(['a', 'b', 'c'])
위의 함수는 생성 시점에 Type을 설정하여 실질적으로 any를 사용한 것과 동일한 효과를 얻을 수 있다.
만약에 배열을 받아서 배열의 길이를 알고 싶다면 어떤 식으로 generic 타입을 지정해야 할까?
const newFunc = <Type>(input: Type): Type => {
console.log(input.length); // error
return input;
};
위에서와 같은 방식은 에러가 발생한다. 어떤 타입이 올지 모르기 때문에 길이를 잴 수 있을지 확실하지 않기 때문이다. 따라서 배열이 올 것을 염두에 두고 있다면 다음과 같은 방식을 활용할 수 있다.
ver.1
const newFunc = <Type>(input: Type[]): Type[] => {
console.log(input.length);
return input; // error
};
ver.2
const newFunc = <Type>(input: Array(Type)): Array(Type) => {
console.log(input.length);
return input; // error
};
newFunc<number>([1, 2, 3]);
newFunc<string>(['a', 'b', 'c']);
generic 함수 타입을 설정하는 방법은 크게 세 가지이다.
let newFunc: <Type>(input: Type) => Type;
let newFunc: { <Type>(input: Type): Type };
ver 1.
interface GenericFn {
<Type>(input: Type): Type;
}
let newFunc: GenericFn;
ver 2.
interface GenericFn <Type> {
(input: Type): Type;
}
let newFunc: GenericFn<number>;
함수를 선언하면서 값 역시 설정해줄 수 있는데 한 부분의 타입이라도 다르면 에러가 발생한다.
const newFunc : <Type>(input: Type): Type => {
console.log(typeof input);
return input;
}
ver 1.
const brandNewFunc: { <Type>(input: Type): Type } = newFunc // ok
ver 2.
const brandNewFunc: { <Type>(input: Type): string } = newFunc // error
class에서도 Generic을 활용하여 재활용이 가능한 class를 만들 수 있다.
class genericClass<Type> {
initialValue: Type,
add: (x: Type, y: Type) => Type
}
let newClass = new genericClass<number>;
newClass.initialValue = 2;
newClass.add = (x, y) => x+y;
console.log(newClass.add(newClass.initialValue, 10)) // 12
let newClass = new genericClass<string>;
newClass.initialValue = "DO";
newClass.add = (x, y) => x+y;
console.log(newClass.add(newClass.initialValue, "HEE")) // DOHEE