제네릭(Generic)
referencetypescriptgenerictype-variable
📌 제네릭의 등장 이유
function func(value:any) {
return value;
}
func("hello");
let num = func(123);
num.toUpperCase(); // ❌ Error: 타입 검사에서는 오류가 없지만 런타임에서 터져버림
func(true);
단순히 매개변수를 반환하는 함수인데, any를 사용하면 TypeScript를 쓰는 이유가 없다. 그리고 타입 검사에서는 오류가 없지만 런타임에서 터져버리는 문제가 발생한다. 그렇다면 unknown을 사용하면 어떨까?
function func(value:unknown) {
return value;
}
func("hello");
let num = func(123);
num.toUpperCase(); // ❌ Error: 'num'의 타입은 'unknown'이므로 'toUpperCase' 속성을 사용할 수 없습니다.
func(true);
분명 number타입을 넘겨줬지만, unknown으로 선언했기 때문에 toUpperCase를 사용할 수 없다. 그렇다고 매번 타입 좁히기를 할 수도 없는 노릇이다.
이러한 문제를 해결하기 위해 제네릭이 등장했다.
📌 제네릭의 기본 문법
function func<T>(value: T): T {
return value;
}
generic은 일단 타입의 값을 받고, 받는 순간 결정하는 범용적인 타입이라고 생각하면 편하다.
<T>: 제네릭을 선언하는 문법. 이때 T는 타입의 이름으로, 사용자가 원하는 이름으로 변경할 수 있다. 이걸 타입 변수라고 한다.- 그리고 필요한 경우, 매개변수와 반환값에 지정한 타입 변수를 사용할 수 있다.
- 제네릭 함수를 호출할 때 타입 변수에 직접 명시하는 것도 가능하다.
func<string>("hello");
📌 타입 변수의 사용법
1. 타입 변수를 매개변수로 사용할 때
function swap<T>(a: T, b: T) {
return [b, a];
}
let result = swap('1', 2); // ❌ Error: 'number' 형식의 인수는 'string' 형식의 매개변수에 할당될 수 없습니다.
이럴 경우에는 여러 개의 타입 변수를 사용하면 된다.
function swap<T, U>(a: T, b: U) {
return [b, a];
}
let result = swap('1', 2); // ✅ Ok
2. 타입 변수를 배열/튜플 타입에 사용할 때
function returnFirst<T>(data: T) {
return data[0]; // ❌ Error: '0' 형식의 식을 'unknown' 인덱스 형식에 사용할 수 없으므로 요소에 암시적으로 'any' 형식이 할당됩니다.
}
let result = returnFirst([1, 2, 3]);
이럴 경우, 아래와 같이 사용하면 된다.
function returnFirst<T>(data: T[]) {
return data[0];
}
let result = returnFirst([1, 2, 3]);
그런데, 만약 타입이 혼합이면?
function returnFirst<T>(data: T[]) {
return data[0];
}
let result = returnFirst(['1', 2, 3]); // result의 타입은 'string | number'가 된다.
우리는 유니온 타입이 아니라 진짜 타입을 알고 싶은 것이다. 따라서,
function returnFirst<T>(data: [T, ...unknown[]]) {
return data[0];
}
let result = returnFirst(['1', 2, 3]); // result의 타입은 'string'이 된다.
이런 특이한 문법으로 해결할 수 있다.
3. 타입 변수에 제약(extends) 걸 때
function getLength<T extends { length: number }>(data: T) {
return data.length;
}
let result1 = getLength([1, 2, 3]);
let result2 = getLength("hello");
let result3 = getLength({ length: 1 });
let result4 = getLength(123); // ❌ Error: 'number' 형식의 인수는 '{ length: number; }' 형식의 매개변수에 할당될 수 없습니다.
map 메서드 타입 확인하기
interface Array<T> { map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; }this를 엄격하게 쓰지는 않는다. 요즘은 대부분 thisArg를 사용하지 않기 때문이라고 생각.
📌 제네릭 인터페이스와 제네릭 클래스
interface Box<T> {
value: T;
}
let box1: Box = { // ❌ Error: 'Box<T>' 제네릭 형식에 1형식 인수가 필요합니다.
value: "hello"
}
let box2: Box<string> = { // ✅ Ok
value: "hello"
}
🎯 실무에서 이런 상황에 유용하다
interface Student {
type: "student",
school: string;
}
interface Teacher {
type: "teacher",
skill: string;
}
interface Person {
name: string;
profile: Student | Teacher;
}
function goToSchool(person: Person) {
if (person.profile.type !== "student") {
console.log("잘못된 프로필 입니다.");
return;
}
console.log(person.profile.school);
}
이런식으로, 타입 좁히기를 하면 되지만, 만약 Person의 profile이 점점 많아진다면 어떻게 될까? 이럴 때 필요한 것이 제네릭 인터페이스이다.
interface Student {
type: "student",
school: string;
}
interface Teacher {
type: "teacher",
skill: string;
}
interface Person<T> {
name: string;
profile: T;
}
function goToSchool(person: Person<Student>) {
console.log(person.profile.school);
}
타입을 좁히지 않더라도 사용할 수 있게 된다.
클래스에서도 사용법은 동일하다.
class Box<T> {
constructor(public value: T[]) { }
push(item: T) {
this.value.push(item);
}
pop() {
return this.value.pop();
}
print() {
console.log(this.value);
}
}
const numberBox = new Box<number>([]);
numberBox.push(1);
📌 프로미스 객체와 제네릭
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(20);
}, 1000)
})
promise.then((result) => {
console.log(result * 10); // ❌ Error: 'result'은(는) 'unknown' 형식입니다.
});
애초에 Promise 타입을 까보면,
interface Promise<T> {
}
new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
이런식으로 실패했을 때 타입은 정할 수 없다. 따라서,
const promise = new Promise<number>((resolve,reject)=>{
setTimeout(()=>{
resolve(20);
// or
reject("error");
},1000)
})
promise.then((result)=>{
console.log(result * 10);
})
promise.catch((error)=>{
if (typeof error === "string") {
console.log(error);
}
})

