*제네릭
은 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법을 말한다.*
Java나 C++등의 정적 타입 언어에서는 함수 및 클래스를 선언하는 시점에서 매개변수 혹은 리턴 타입을 정의해야하기 때문에 기본적으로는 특정 타입을 위해 만들어진 클래스나 함수를 다른 타입을 위해 재사용할 수가 없다. 때문에 제네릭을 통해 함수와 클래스의 범용적인 사용을 가능케 한다.
[ 타입 재사용에 용이한 함수 제네릭 / 제네릭을 사용하는 이유 ]
*// 일반적인 타입 정의
// 함수 호출 시 number 타입만 사용이 가능하다.
// Bad*
function checkNotNullBad(arg: number | null): number {
if (arg == null) {
throw new Error('not valid number!');
}
return arg;
}
*// 유동적인 타입 정의*
*// 함수 호출 시 여러 타입사용은 가능하지만 any 를 사용하는건 좋지 않다.
// Very Bad*
function checkNotNullAnyBad(arg: any | null): any {
if (arg == null) {
throw new Error('not valid number!');
}
return arg;
}
const result = checkNotNullAnyBad(123);
*// 유동적인 타입 정의
// 함수 호출 시 사용된 값(인수)에 의해 타입이 정의되며, 타입이 보장된다.
// Good*
function checkNotNull<T>(arg: T | null): T {
if (arg == null) {
throw new Error('not valid number!');
}
return arg;
}
const number = checkNotNull(123);
*// 또는 const number = checkNotNull<number>(123);*
const boal: boolean = checkNotNull(true);
*// 또는 const boal: boolean = checkNotNull<boolean>(true);*
JavaScript는 원래 타입 선언이 필요하지 않고, 그렇기에 특정 타입을 위해 만들어진 클래스나 함수도 타입 에러를 런타임에서 일으킬 뿐이다. 코드를 실행시키기 전까지는 함수와 클래스가 모든 타입에 대응한다. 그렇기 때문에 JavaScript에서는 제네릭이란 말을 들을 일이 없다.
정적 타입 언어에서도 이렇게 특정 타입을 위해 만들어진 함수 혹은 클래스를 보다 범용적으로 재사용하기 위한 요구가 있기 때문에 제네릭이라는 프로그래밍 기법이 생긴게 아닐까한다. TypeScript도 마찬가지로 정적 타입 언어이기 때문에, 기본적으로 타입을 정의한 함수 혹은 클래스는 모두 다른 타입에 재사용할 수 없다. 제네릭을 사용하지 않는다면 말이다.
문법
클래스나 함수의 식별자 선언부에 <T> 라는 문법이 추가된 것을 확인할 수 있다. 제네릭을사용하겠다는 의미로 꺽쇠(Angle brackets)를 넣고 그 안에 타입으로 사용되는 식별자를 집어넣는다.
T는 Type의 약자로 다른 언어에서도 제네릭을 선언할 때 관용적으로 많이 사용된다. 이 부분에서 식별자로 사용할 수 있는 것이라면 무엇이든 들어갈 수 있다. 이를테면 $나 _도 가능하다. 하지만 대개의 경우 T를 사용한다. 여기에서 T를 (Type variable)라고 한다.
이렇게해서 클래스나 함수에서 제네릭을 사용하겠다고 선언한 경우 T 는 해당 클래스에서 사용할 수 있는 특정한 타입이 된다. 클래스에서 제네릭 사용법은 아래와 같다. 그저 생성자를 호출하여 객체를 만들 때 T 로 사용될 타입을 지정해주기만 하면 된다.
*// 제네릭 클래스*
interface Either<L, R> {
left: () => L; *// property*
right: () => R; *// property*
center: <T>(value: T) => T; *// property
//-> Either<L, R>.center: <T>(value: T) => T*
*//center<T>(value: T): T; // method
//-> Either<L, R>.center<T>(value: T): T*
}
*// 제네릭 클래스*
class SimpleEither<L, R> implements Either<L, R> {
constructor(private leftValue: L, private rightValue: R) {}
left(): L {
return this.leftValue;
}
right(): R {
return this.rightValue;
}
*// 제네릭 함수*
center<T>(value: T): T {
return value;
}
}
*// 세부적인 타입을 인자로 받아서 정말 추상적인 타입으로 다시 리턴하는 함수는 💩💩💩*
function eitherObjBad(eitherobj: Either<number, number>) {
eitherobj.left();
return obj;
}
*// 제네릭 함수 (상속된 타입 변수 사용)*
function eitherObj<O extends Either<number, number>>(eitherobj: O) {
eitherobj.left();
return eitherobj;
}
const either: Either<number, number> = new SimpleEither(4, 5);
*// 또는 const either: Either<number, number> = new SimpleEither<number, number>(4, 5);*
either.left(); // 4
either.right(); // 5
either.center(1); // 1
eitherObj(either).center(1); // 1
*// 또는 eitherObj<Either<number, number>>(either).center(1); // 1*
상속된 타입 변수
타입 변수는 기본에 사용하고 있는 타입을 상속할 수도 있다. 이 점을 이용하면 입력 받을 변수의 타입을 제한할 수 있다. 또한 코드 에디터가 해당 타입의 메소드(method)나 프로퍼티(property) 를 예측할 수 있으므로 자동 완성이 된다.