함수나 인터페이스, 타입 별칭, 클래스 등에서 다양한 타입을 사용해서 동작할 수 있도록 해주는 문법입니다.
function func(value: any) {
return value;
}
let num = func(10);
// any 타입
let str = func("string");
// any 타입
매개변수에 다양한 타입을 제공받게 하기 위해 any
타입으로 설정했습니다. unknown
타입으로도 가능합니다.
그리고 위 함수는 매개변수를 그대로 반환하는 함수입니다.
함수의 결과로 num
에는 10, str
은 string 이 할당됩니다.
하지만 num
과 str
의 타입은 any
타입으로 추론됩니다.
함수의 반환값 타입은 return
문 기준으로 추론되기 때문입니다.
매개변수 value
를 unknown
타입으로 설정해도 마찬가지로 num
과 str
은 unknown
타입으로 추론됩니다.
이 같은 문제 때문에 다음과 같은 예측하지 못하는 상황이 발생하게 됩니다.
function func(value: any) {
return value;
}
let num = func(10);
let str = func("string");
num.toUpperCase()
function func(value: unknown) {
return value;
}
let num = func(10);
// unknown 타입
let str = func("string");
// unknown 타입
num.toUpperCase(); // ❌
num.toFixed(); // ❌
각 타입이 any
혹은 unknown
타입이기 때문에 이 타입의 메서드가 아니어도 호출 가능한 상황이 나오기도 하고, 해당 타입의 메서드가 맞는데도 호출 불가능한 상황이 나오기도 합니다.
이를 위해서 필요한 것이 제네릭 함수입니다.
제네릭은 '포괄적인' 이라는 의미가 있습니다. 즉, 제네릭은 타입을 일반화하여 다양한 타입을 적용할 수 있도록 하기 위해 사용됩니다.
이 제네릭을 활용한 제네릭 함수도 마찬가지로 다양한 타입의 값을 적용하여 사용할 수 있는 범용적인 함수입니다.
function func<T>(value: T): T {
return value;
}
let num = func(10);
// number 타입
위와 같이 제네릭 타입(타입 변수)를 선언할 수 있고 타입 변수에 어떤 타입이 할당될 지는 함수 호출 시에 결정됩니다.
즉, 매개변수에 제공되는 값을 기준으로 타입 변수의 타입을 추론합니다.
10을 인수로 넘겨 호출했기 때문에 T
는 number
타입으로 추론됩니다.
제네릭 함수를 호출할 때 타입 변수에 할당할 타입을 직접 명시하는 것도 가능합니다.
function func<T>(value: T): T {
return value;
}
let arr = func<[number, number, number]>([1, 2, 3]);
T에 튜플 타입이 할당되고 매개변수와 반환값 타입 모두 튜플 타입이 됩니다.
제네릭은 인터페이스에도 적용할 수 있습니다.
인터페이스에 타입 변수를 선언하여 사용하면 됩니다.
interface KeyPair<K, V> {
key: K;
value: V;
}
let keyPair: KeyPair<string, number> = {
key: "key",
value: 0,
};
let keyPair2: KeyPair<boolean, string[]> = {
key: true,
value: ["1"],
};
제네릭 인터페이스를 인덱스 시그니처와 함께 사용하면 더 유연한 객체 타입을 정의할 수 있습니다.
interface Map<V> {
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
stringMap
은 key는 string
타입이고 value는 string
타입인 모든 프로퍼티를 포함하는 객체입니다.
booleanMap
도 마찬가지입니다.
타입 별칭에도 제네릭을 적용할 수 있습니다.
type Map2<V> = {
[key: string]: V;
};
let stringMap2: Map2<string> = {
key: "string",
};
클래스에도 제네릭을 적용할 수 있습니다.
class List<T> {
constructor(private list: T[]) {}
push(data: T) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
const numberList = new List([1, 2, 3]);
const stringList = new List(["1", "2"]);
클래스 이름 뒤에 꺽쇠와 함께 타입 변수를 선언하면 됩니다.
클래스는 생성자를 통해 타입 변수의 타입을 추론할 수 있습니다. 따라서 클래스를 통해 인스턴스를 생성할 때 인수를 전달하는 값이 있는 경우 타입 변수에 할당할 타입을 생략할 수 있습니다.
자바스크립트의 내장 클래스 Promise
는 타입스크립트에서 제네릭 클래스로 별도로 선언되어 있습니다.
Promise
를 생성할 때 타입 변수에 할당할 타입을 설정해주면 해당 타입이 resolve 결과값의 타입이 됩니다.
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
// 결과값 : 20
resolve(20);
}, 3000);
});
promise.then((response) => {
// response는 number 타입
console.log(response);
});
promise.catch((error) => {
if (typeof error === "string") {
console.log(error);
}
});
비동기 처리(서버로부터 데이터를 받아오는 등)를 할 때에는 보통 어떤 객체를 받아오게 됩니다.
특정 객체의 타입을 미리 정의하고 해당 타입을 Promise
의 결과값 타입으로 타입 변수를 설정하면 됩니다.
interface Post {
id: 1,
title: '게시글 제목'
content: '게시글 본문'
}
function fetchPost() {
return new Promise<Post>((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 본문",
});
}, 3000);
});
}
Promise
의 생성자 함수에 타입 변수를 할당하는 것 대신 비동기 처리를 진행하는 함수 fetchPost
함수의 결과값 타입을 제네릭을 포함한 Promise
타입을 반환하도록 하면 한 눈에 어떤 결과값을 반환하는지 파악하기 좋습니다.
function fetchPost(): Promise<Post> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 본문",
});
}, 3000);
});
}