C++ 에서의 제네릭과 동일하다.
function getText<T>(text: T): T {
return text;
}
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
- 이렇게 T에 자료형이 들어가며 동작한다.
타입스크립트에서 쓰는 이유
function echo(text: string): string { return text; }
function echo(text: any): any { return text; }
- 만약 모든 타입에 대해서 허용하고 싶다면 두 번째 처럼 any 를 쓰면 된다.
- 단, any 타입은 타입검사를 하지 않기 때문에, 동작에는 문제가 없지만 파라미터로 어떤 타입이 들어갔고, 어떤 값이 반환 되는지 알 수 없다. 그래서 제네릭을 사용한다.
- 그래서 제네릭을 사용하면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 되어 확인이 가능하다.
// 사용법 #1
const text = echo<string>("Hello Generic");
// 사용법 #2
const text = echo("Hello Generic");
제네릭 타입 변수
제네릭을 사용할 때 주의해야할 점이 있다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
컴파일러는 위 상황에서 text가 length 를 갖고있는지 모르기 때문에 에러를 발생시킨다.
다만, 배열의 경우는 제네릭에 타입을 줄 수 있다.
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용합니다.
return text;
}
function logText<T>(text: Array<T>): Array<T> {
console.log(text.length);
return text;
}
T[] 는 T에 무슨 자료형의 배열이 오든 간에, 인자 값으로 배열 형태의 T가 들어오게 되므로 length 를 사용할 수 있다.
제네릭 타입
제네릭 인터페이스 사용법을 알아보자.
function logText<T>(text: T): T {
return text;
}
// #1
let str: <T>(text: T) => T = logText;
// #2
let str: {<T>(text: T): T} = logText;
#1
- 제네릭 함수 타입을 변수에 할당함.
- 타입 T를 받아와서 그 타입 그대로 return 하는 함수. (ex. str<string>(’hi’);)
#2
- 객체 타입처럼 제네릭을 선언함.
- str 변수는 제네릭 함수를 하나만 가진 객체처럼 생겼지만, 실제로는 그 함수 그 자체.
- 동작은 #1 과 동일.
이런 식으로 제네릭 인터페이스도 아래와 같이 작성 가능하다.
interface GenericLogTextFn {
<T>(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn = logText;
인자 타입을 강조하고 싶다면
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는 미정의 자료형이므로 length 메소드를 호출한다던가 하는 식의 사용은 불가능하다 했다.
하지만 아래와 같은 방법을 쓰면 해당 타입을 정의하지 않고도 length를 쓸 수 있다.
interface LengthWise {
length: number;
}
function logText<T extends LengthWise>(text: T): T {
console.log(text.length);
return text;
}
logText(10); // Error, 숫자 타입에는 `length`가 존재하지 않으므로 오류 발생
logText({ length: 0, value: 'hi' }); // `text.length` 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
이렇게 쓰면 타입에 대해 강제는 아니지만, 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> 부분에서 첫 번째 인자로 받는 객체에 없는 속성을 접근을 제한할 수 있다.
- 즉, O는 T의 부분집합이어야 한다라고 제약을 걸어두는 것이다.
'언어 > TypeScript' 카테고리의 다른 글
| 타입 호환성 (0) | 2025.04.15 |
|---|---|
| 타입 추론 (0) | 2025.04.15 |
| Class 특성 (0) | 2025.04.14 |
| Union Type, Intersection Type (0) | 2025.04.14 |
| enum 자료형 (0) | 2025.04.13 |