제네릭(Generics)의은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용됩니다.
특히 한가지 타입보다는 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 자주 사용됩니다.
제네릭은 타입을 함수의 파라미터처럼 사용하도록 해주는 것인데요.
function getName(name) {
return name;
}
위의 코드는 name이라는 인자를 받아 name을 그대로 리턴을 해주는데 이때 name의 타입은 정해져 있지가 않습니다.
문자열, 숫자, boolean 등 어떤 값이 들어가더라도 그대로 반환합니다.
function getName<T>(name: T): T {
return name;
}
위의 코드는 함수에 제네릭 적용된 형태입니다.
이제 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있습니다.
function getName<string>(name: T): T {
return name;
}
getName<string>('hello');
위 함수에서 제네릭 타입이 <string>이 되는 이유는 getName() 함수를 호출할 때 제네릭 값으로 string을 넘겼기 때문입니다.
그리고 나서 함수의 인자로 hello 라는 값을 아래와 같이 넘기게 되면 getName 함수는 아래와 같이 타입을 정의한 것과 같습니다.
function getName<string>(name: string): string {
return name;
}
위 함수는 입력 값의 타입이 string이면서 반환 값 타입도 string이어야 합니다.
제네릭을 사용하는 이유
또 다른 예제를 살펴보겠습니다.
function logText(text: string): string {
return text;
}
위 코드는 인자를 하나 넘겨 받아 반환해주는 함수인데 이 함수의 인자와 반환 값은 모두 string으로 지정되어 있습니다.
function logText(text: any): any {
return text;
}
만약 여러 가지 타입을 허용하고 싶다면 아래와 같이 any를 사용할 수 있습니다.
any의 경우 타입을 검사하지 않기 떄문에 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지 알 수가 없습니다.
제네릭을 사용하면 이러한 문제점을 해결할 수 있습니다.
function logText<T>(text: T): T {
return text;
}
<T> 라는 제네릭을 추가했습니다.
그리고 파라미터와 리턴타입에 모두 T 라는 타입을 추가하였습니다.
이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 되고 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있습니다.
이렇게 선언한 함수는 아래와 같이 2가지 방법으로 호출할 수 있습니다.
// #1
const text = logText<string>("Hello Generic");
// #2
const text = logText("Hello Generic");
일반적으로 두 번째 방법이 코드가 더 짧기도 하고 가독성이 좋기 때문에 많이 사용되는 방법입니다.
하지만 복잡한 코드에서 두 번째 코드로 타입 추정이 되지 않는 경우가 있는데 이럴때는 첫 번째 방법을 사용하면 됩니다.
제네릭 타입 변수
제네릭을 사용하기 시작하면 컴파일러에서 인자에 타입을 넣어달라는 경고를 보게 됩니다.
function logText<T>(text: T): T {
// console.log(text.length); // Error: T doesn't have .length
return text;
}
만약 여기서 함수의 인자로 받은 값의 length를 확인하고 싶다면 대부분 length를 사용할 것입니다.
text에 .length가 있다는 단서는 어디에도 없기 때문에 length를 반환하려고 하면 컴파일러에서 에러를 발생시킵니다.
위의 코드에서 함수의 인자와 반환 값에 대한 타입을 정하진 않았지만, 입력 값으로 어떤 타입이 들어왔고 반환 값으로 어떤 타입이 나가는지 알 수 있습니다.
따라서, 함수의 인자와 반환 값 타입에 any를 명시한 것과 같은 동작을 하게 됩니다.
이러한 특성 때문에 현재 인자인 text에 문자열이나 배열이 들어와도 아직은 컴파일러 입장에서 .length를 허용할 수가 없습니다.
number가 들어왔을 때는 .length 코드가 유효하지 않기 때문입니다.
이런 경우에는 제네릭에 타입을 줄 수가 있습니다.
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용합니다.
return text;
}
이 제네릭 함수 코드는 일단 T라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T를 받습니다.
예를 들면, 함수에 [1,2,3]처럼 숫자로 이뤄진 배열을 받으면 반환 값으로 number를 돌려줍니다.
이런 방식으로 제네릭을 사용하면 유연한 방식으로 함수의 타입을 정의해줄 수 있고 좀 더 명시적으로 제네릭 타입을 선언할 수 있습니다.
function logText<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
제네릭 타입
제네릭 인터페이스에 대해 알아보겠습니다. 아래의 두 코드는 같은 의미입니다.
function logText<T>(text: T): T {
return text;
}
// #1
let str: <T>(text: T) => T = logText;
// #2
let str: {<T>(text: T): T} = logText;
위와 같은 변형 방식으로 제네릭 인터페이스 코드를 다음과 같이 작성할 수 있습니다.
interface GenericLogTextFn {
<T>(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn = logText; // Okay
위 코드에서 만약 인터페이스에 인자 타입을 강조하고 싶다면 아래와 같이 변경할 수 있습니다.
interface GenericLogTextFn<T> {
(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn<string> = logText;
이와 같은 방식으로 제네릭 인터페이스 뿐만 아니라 클래스도 생성할 수 있습니다.
다만, 이넘(enum)과 네임스페이스(namespace)는 제네릭으로 생성할 수 없습니다.
제네릭 클래스
제네릭 클래스는 앞에서 살펴본 제네릭 인터페이스와 비슷합니다. 코드를 보겠습니다.
class GenericMath<T> {
pi: T;
sum: (x: T, y: T) => T;
}
let math = new GenericMath<number>();
제네릭 클래스를 선언할 때 클래스 이름 오른쪽에 <T>를 붙여줍니다.
그리고 해당 클래스로 인스턴스를 생성할 때 타입에 어떤 값이 들어갈 지 지정하면 됩니다.
조금 전에 살펴본 인터페이스처럼 제네릭 클래스도 클래스 안에 정의된 속성들이 정해진 타입으로 잘 동작하게 보장할 수 있습니다.
WARNING
참고! Generic classes are only generic over their instance side rather than their static side, so when working with classes, static members can not use the class’s type parameter
제네릭 제약 조건
앞에서 제네릭 타입 변수에서 살펴본 내용 말고도 제네릭 함수에 어느 정도 타입 힌트를 줄 수 있는 방법이 있습니다.
잠시 이전 코드를 보겠습니다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 납니다.
이럴 때 만약 해당 타입을 정의하지 않고도 length 속성 정도는 허용하려면 아래와 같이 작성합니다.
interface LengthWise {
length: number;
}
function logText<T extends LengthWise>(text: T): T {
console.log(text.length);
return text;
}
위와 같이 작성하게 되면 타입에 대한 강제는 아니지만 length에 대해 동작하는 인자만 넘겨받을 수 있게 됩니다.
logText(10); // Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' }); // `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
객체의 속성을 제약하는 방법
두 객체를 비교할 때도 제네릭 제약 조건을 사용할 수 있습니다.
function getProperty<T, O extends keyof T>(obj: T, key: O) {
return obj[key];
}
let obj = { a: 1, b: 2, c: 3 };
getProperty(obj, "a"); // okay
getProperty(obj, "z"); // error: "z"는 "a", "b", "c" 속성에 해당하지 않습니다.
제네릭을 선언할 때 <O extends keyof T> 부분에서 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한하였습니다.
'TS 관련 > TypeScript' 카테고리의 다른 글
[TypeScript] Abstract Class(추상 클래스) (0) | 2022.07.23 |
---|---|
[TypeScript] Intersection Type(인터섹션 타입) (0) | 2022.07.21 |
[TypeScript] 접근 제한자(Access Modifiers), Getter와 Setter (0) | 2022.07.20 |
[TypeScript] 생성자(Constructor) (0) | 2022.07.19 |
[TypeScript] Class와 객체(OOP 프로그래밍) (0) | 2022.07.18 |