- 제네릭이란?
- 언제 유용할까?
- 함수나 메서드의 인수에 범용성을 부여하고 싶을 때
- 보충 설명: 인수의 수가 다른 오버로드
- 제네릭을 클래스 타입으로
- 타입 인수 기본값 설정
- 타입 인수에 제약을 두기
- 제네릭 메서드
제네릭이란?
Java에서는 제네릭스(Generics)라고 불리기도 합니다.
언어마다 이 용어의 표현이 다르죠.
TypeScript에서도 제네릭스나 제너릭스라고 불리기도 합니다.
제네릭은 "총칭형(総称型)"이라고도 불리며, 임의의 클래스나 메서드에 대해 임의의 타입을 연결할 수 있는 구조입니다.
언제 유용할까?
배열에서 다루는 타입을 지정하고 싶을 때
예를 들어 배열 변수를 정의할 때, 이 배열이 특정 타입을 다룬다는 것을 알 수 있으면 좋습니다.
그리고 지정된 타입 이외의 값을 설정하려 할 때나, 지정된 타입에 없는 멤버를 설정하려 할 때 오류를 발생시켜주면 더욱 좋습니다.
이러한 기능을 제공해주는 것이 제네릭입니다.
예제 코드를 살펴봅시다.
// Array에서 제네릭 타입을 지정할 때 사용하는 인터페이스
interface IFoo {
foo: string;
bar: number;
baz: boolean;
}
// IFoo 타입의 배열을 선언
// 아래 코드는 const aFoo: IFoo[]와 동일
const aFoo: Array<IFoo> = [];
// 이는 설정하려는 값의 타입이 다르므로 오류가 발생합니다.
// Argument of type 'number' is not assignable to parameter of type 'IFoo'.(2345)
aFoo.push(1);
// 이 경우에도 설정하려는 값의 타입이 다르므로 오류가 발생합니다.
// Argument of type 'string' is not assignable to parameter of type 'IFoo'.(2345)
aFoo.push("foo");
// 변수를 선언할 때 지정한 IFoo 타입에 맞는 데이터이므로 괜찮습니다.
aFoo.push({
foo: "foo",
bar: 100,
baz: false,
});
이렇게 Array에서 다룰 타입을 지정할 수 있고, 다른 타입을 다루려고 하면 오류를 반환합니다.
참고로 IFoo 타입의 배열을 선언할 때 아래와 같은 방법으로도 작성할 수 있습니다.
// IFoo 타입의 배열 선언
const aFoo: IFoo[] = [];
함수나 메서드의 인수에 범용성을 부여하고 싶을 때
TypeScript에서는 메서드 오버로드도 지원하지만, Java나 C# 등의 오버로드와는 다릅니다.
다음 코드는 메서드 오버로드의 예시입니다.
/**
* 메서드 인수의 타입이 다른 오버로드 구현 예시
* 오버로드를 공용 타입으로 구현하고 있습니다.
*/
// 오버로드할 메서드 시그니처를 선언
interface IFoo {
// 메서드 시그니처 부분
foo(arg: string);
foo(arg: string[]);
}
class CFoo implements IFoo {
// 인수를 공용 타입으로 지정하여 범용성을 확보
foo(arg: string | string[]) {
console.log(arg);
}
}
const cFoo = new CFoo();
cFoo.foo("foo");
cFoo.foo(["foo", "bar"]);
// Logs:
// foo
// ["foo", "bar"]
이 코드에서는 메서드의 실제 구현이 인수를 공용 타입으로 정의하고 있습니다.
다루고 싶은 타입이 늘어나면 그만큼 공용 타입에 추가해야 하므로 다소 복잡합니다.
이 복잡함을 제네릭을 사용하여 해결할 수 있습니다. 아래는 제네릭을 사용한 구현입니다.
/**
* 메서드 인수의 타입이 다른 오버로드 구현 예시
* 오버로드를 제네릭을 사용하여 구현함으로써 임의의 타입을 다룰 수 있습니다.
*/
// 오버로드할 메서드 시그니처를 선언
interface IFoo {
// 메서드 시그니처 부분
foo<T>(arg: T);
}
class CFoo implements IFoo {
foo<T>(arg: T) {
console.log(arg);
}
}
const cFoo = new CFoo();
cFoo.foo<string>("foo");
cFoo.foo<number>(100);
cFoo.foo<{}>({ foo: "foobar" });
cFoo.foo<string[]>(["foo", "bar"]);
// Logs:
// foo
// 100
// { foo: "foobar" }
// ["foo", "bar"]
앞의 코드와 마찬가지로 이 구현 방법도 Java나 C#과 같은 오버로드와는 다소 차이가 있지만, 인수를 공용 타입으로 지정하는 것보다 인터페이스가 깔끔해졌습니다.
실제 업무에서 사용할 때는 구체적인 클래스의 역할과 전달되는 인수의 타입에 주의해야 하지만, 제네릭을 사용할 수 있는 상황을 상상할 수 있게 되었습니다.
보충 설명: 인수의 수가 다른 오버로드
위의 코드에서는 인수의 수가 같은 경우에만 대응할 수 있습니다.
인수의 수가 다른 오버로드에 대응하려면 제네릭을 사용하지 않는 구현 예시처럼 두 번째 인수 이후를 옵션으로 지정해야 합니다.
제네릭을 사용한 경우에도 동일하게 구현할 수 있습니다.
/**
* 메서드 인수의 수가 다른 오버로드 구현 예시
* 이 경우 제네릭을 사용한 구현에서도 두 번째 인수 이후를 옵션으로 지정해야 합니다.
*/
// 오버로드할 메서드 시그니처를 선언
interface IFoo {
// 메서드 시그니처 부분
foo(arg: string, arg2?: string, arg3?: string);
}
class CFoo implements IFoo {
// 두 번째 인수 이후를 옵션으로 지정하여 범용성을 확보
foo(arg: string, arg2?: string, arg3?: string) {
console.log(arg);
if (arg2) {
console.log(`${arg}, ${arg2}`);
}
if (arg3) {
console.log(`${arg2}, ${arg2}, ${arg3}`);
}
}
}
const cFoo = new CFoo();
cFoo.foo("foo");
cFoo.foo("foo", "bar");
cFoo.foo("foo", "bar", "baz");
제네릭을 클래스 타입으로
제네릭은 클래스 타입으로도 사용할 수 있습니다.
기본
제네릭을 클래스 타입으로 사용하려면 다음과 같이 클래스 이름 뒤에 <...>
를 지정합니다.
/**
* 임의의 타입을 다루는 클래스
* 제네릭으로 지정된 타입은 해당 클래스에서 다루는 타입이 됩니다.
*/
class GFoo<T> {
value: T;
getValue(): T {
return this.value;
}
}
// string을 지정하여 인스턴스를 생성
// - 이를 통해 GFoo는 string을 다루는 인스턴스로 생성됨
// - GFoo의 필드인 value에는 string만 설정 가능
const gFoo = new GFoo<string>();
gFoo.value = 'Foo';
console.log(gFoo.getValue()); // Foo
// value에 string 이외의 값을 설정하면 오류 발생
//
// Type 'boolean' is not assignable to type 'string'.(2322)
// (property) GFoo<string>.value: string
gFoo.value = false;
// number도 마찬가지
//
// Type 'number' is not assignable to type 'string'.(2322)
// (property) GFoo<string>.value: string
gFoo.value = 1;
클래스에 대해 제네릭을 사용하여 타입을 지정하면, 해당 클래스의 인스턴스가 다루는 타입을 임의로 설정할 수 있음을 알 수 있습니다.
제네릭을 사용함으로써 인스턴스 생성 시 다룰 타입의 자유도가 높아졌을 뿐만 아니라 지정한 타입 이외의 값은 오류로 처리하는 엄격함도 얻을 수 있게 되었습니다.
응용: 여러 개의 타입 인수 지정
클래스에서 다룰 타입은 여러 개 지정할 수 있습니다.
방법은 타입 인수를 지정할 때 ,
로 구분하는 것입니다.
/**
* 타입 인수를 여러 개 지정할 수 있는 클래스
*/
class GFoo<T, R> {
tValue: T;
rValue: R;
getTValue(): T {
return this.tValue;
}
getRValue(): R {
return this.rValue;
}
}
// 타입 인수를 2개 받는 클래스이므로 인스턴스 생성 시에도 타입을 여러 개 지정
// 여기서는 string과 number를 다루는 인스턴스를 생성
const gFoo = new GFoo<string, number>();
gFoo.tValue = 'Foo';
gFoo.rValue = 100;
console.log({
tValue: gFoo.getTValue(),
rValue: gFoo.getRValue(),
});
// Logs:
// { tValue: "Foo", rValue: 100 }
타입 인수 기본값 설정
타입 인수에는 기본값을 설정할 수도 있습니다.
기본값을 지정하여 클래스를 정의한 경우:
- 타입을 지정하지 않고 인스턴스를 생성하기
- 타입을 지정하여 인스턴스를 생성하기
두 가지 방법으로 인스턴스를 생성할 수 있습니다.
전자의 경우 기본값 타입을 다루는 인스턴스가 생성되고, 후자의 경우 지정된 타입을 다루는 인스턴스가 생성됩니다.
물론 기본값 타입을 지정하여 인스턴스를 생성할 수도 있습니다.
/**
* 타입 인수의 기본값을 지정하여 임의의 타입을 다루는 클래스
*/
class GFoo<T = string> {
tValue: T;
getTValue(): T {
return this.tValue;
}
}
///////////////////////////////////////////////////
// 타입 인수를 지정하지 않고 인스턴스를 생성하기
///////////////////////////////////////////////////
// 기본값으로 string을 지정했으므로 타입 인수 없이 인스턴스를 생성할 수 있음
const gFooDefault = new GFoo();
gFooDefault.tValue = 'Foo';
console.log({
tValue: gFooDefault.getTValue(),
});
// Logs:
// { tValue: "Foo" }
// 이 상태에서 tValue에 number를 지정하면 오류가 발생
// Type 'number' is not assignable to type 'string'.(2322)
// const gFoo: GFoo<string>
gFooDefault.tValue = 100;
///////////////////////////////////////////////////
// 타입 인수를 지정하여 인스턴스를 생성하기
///////////////////////////////////////////////////
// 이번에는 타입 인수를 지정하여 인스턴스를 생성
const gFooNumber = new GFoo<number>();
gFooNumber.tValue = 100;
console.log({
tValue: gFooNumber.getTValue(),
});
// Logs:
// { tValue: 100 }
// 타입 인수에 number를 지정해 인스턴스를 생성했으므로 string을 설정하려 하면 오류가 발생
// Type 'string' is not assignable to type 'number'.(2322)
// (property) GFoo<number>.tValue: number
gFooNumber.tValue = "Foo";
///////////////////////////////////////////////////
// 타입 인수 기본값과 동일한 타입을 지정하여 인스턴스를 생성하기
///////////////////////////////////////////////////
// 이번에는 타입 인수 기본값과 동일한 타입을 지정하여 인스턴스를 생성
const gFooString = new GFoo<string>();
gFooString.tValue = "Foo";
console.log({
tValue: gFooString.getTValue(),
});
// Logs:
// { tValue: "Foo" }
// 타입 인수에 string을 지정해 인스턴스를 생성했으므로 number를 설정하려 하면 오류가 발생
// Type 'number' is not assignable to type 'string'.(2322)
// (property) GFoo<string>.tValue: string
gFooString.tValue = 100;
타입 인수에 제약을 두기
여기서 말하는 "제약"이란 특정 클래스를 상속한 클래스만 허용한다는 의미입니다.
인스턴스를 생성할 때 제네릭 타입 인수에는 임의의 타입을 지정할 수 있지만, 다룰 타입을 제한하고 싶은 경우가 있습니다.
그럴 때 유용한 방법을 소개합니다.
class BaseFoo {
value: string;
}
class ChildFoo extends BaseFoo {
value2: number;
}
/**
* 제네릭으로 다룰 타입을 특정 클래스 또는 그 하위 클래스에 제한
*/
class GFoo<T extends BaseFoo> {
tValue: T;
getTValue(): T {
return this.tValue;
}
}
////////////////////////////////////////////////////
// 타입 인수에 하위 클래스를 지정하여 인스턴스를 생성하기
////////////////////////////////////////////////////
const gFooChild = new GFoo<ChildFoo>();
// 하위 클래스의 인스턴스를 설정
gFooChild.tValue = new ChildFoo();
console.log({
value: gFooChild.getTValue(),
instance: gFooChild.getTValue() instanceof ChildFoo,
});
// Logs:
// { instance: true, value: ChildFoo }
//--------------------------------------------------
// 베이스 클래스의 인스턴스를 설정하면 오류 발생
// (이번 예제에서는 베이스 클래스와 하위 클래스 간 필드에 차이가 있기 때문)
// (베이스 클래스와 하위 클래스의 필드에 차이가 없으면 오류가 발생하지 않음)
//
// Property 'value2' is missing in type 'BaseFoo' but required in type 'ChildFoo'.(2741)
gFooChild.tValue = new BaseFoo();
////////////////////////////////////////////////////
// 타입 인수에 베이스 클래스를 지정해도 인스턴스 생성 가능
////////////////////////////////////////////////////
// 베이스 클래스의 인스턴스를 설정
const gFooBase = new GFoo<BaseFoo>();
gFooBase.tValue = new BaseFoo();
console.log({
value: gFooBase.getTValue(),
instance: gFooBase.getTValue() instanceof BaseFoo,
});
// Logs:
// { instance: true, value: BaseFoo }
//--------------------------------------------------
// 하위 클래스의 인스턴스를 설정해도 문제 없음
gFooBase.tValue = new ChildFoo();
console.log({
value: gFooBase.getTValue(),
instance: gFooBase.getTValue() instanceof ChildFoo,
});
// Logs:
// { instance: true, value: ChildFoo }
// 단, 베이스 클래스에 없는 필드에는 접근할 수 없음
// Property 'value2' does not exist on type 'BaseFoo'. Did you mean 'value'?(2551)
gFooBase.getTValue().value2 = '#1';
////////////////////////////////////////////////////
// BaseFoo를 상속하지 않은 타입은 지정할 수 없음
////////////////////////////////////////////////////
// Type 'string' does not satisfy the constraint 'BaseFoo'.(2344)
const gFooNotExtends = new GFoo<string>();
제네릭 메서드
다음과 같은 특징을 가지고 있습니다.
- 인수, 반환값, 로컬 변수의 타입을 메서드를 호출할 때 결정할 수 있다.
- 메서드 이름 바로 뒤에
<...>
형식으로 타입 인수를 선언한다.
제네릭 메서드 타입
// 타입 인수가 하나인 경우
function gFuncFoo<T>(arg1: T, arg2: T): T {}
// 타입 인수가 두 개인 경우
function gFuncFoo<T, U>(arg1: T, arg2: U): U {}
예제 코드
/**
* 임의의 타입을 인수로 받아 그 인수를 객체로 반환하는 함수
*/
function gFuncFoo<T>(arg1: T, arg2: T): { arg1: T, arg2: T } {
return {
arg1,
arg2
};
}
console.log(gFuncFoo("foo", "bar"));
// Logs:
// { arg1: "foo", arg2: "bar" }
/**
* 임의의 타입을 여러 개 인수로 받아 그 인수를 튜플로 반환하는 함수
*/
function gFuncFooMulti<T, U>(arg1: T, arg2: U): [T, U] {
return [
arg1,
arg2
];
}
console.log(gFuncFooMulti("foo", 100));
// Logs:
// ["foo", 100]
class Foo {
/**
* 임의의 타입 배열을 인수로 받아 그 배열을 결합해 반환하는 메서드
*/
static gFuncConcat<T>(data: T[], data2: T[]): T[] {
return data.concat(data2);
}
}
console.log(Foo.gFuncConcat(["foo"], ["bar"]));
// Logs:
// ["foo", "bar"]
이렇게 TypeScript에서 제네릭을 사용하여 다양한 타입의 코드를 쉽게 작성하고, 타입 안전성을 유지하며 재사용 가능한 코드를 작성할 수 있습니다.
'Javascript' 카테고리의 다른 글
TypeScript 고급 타입 마스터하기 - 객체 키 추출부터 infer 활용까지 (1) | 2024.05.15 |
---|---|
TypeScript의 효율적인 타입 관리 - any 없이 타입 정의, 타입 파생 및 제네릭 마스터하기 (1) | 2024.05.15 |
TypeScript 4.5 이상 버전에서 추가된 TSConfig 옵션 살펴보기 (0) | 2024.05.12 |
TypeScript를 다른 도구로 다루기 위한 컴파일러 옵션 (0) | 2024.05.11 |
JSON-LD로 구조화된 데이터를 Next.js 사이트에 추가하여 SEO 개선하기 (0) | 2024.05.03 |