제네릭(Generic)이란 어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 문법입니다.
// 일반 함수
function func(value: any) {
return value;
}
let num = func(10); // num: any
let bool = func(true); // bool: any
let str = func("string"); // str: any
function func1(value: unknown) {
return value;
}
str.toUpperCase(); // ❌ No, str: unknown
num.toFixed(); // ❌ No, num: unknown
// 제네릭 함수
function func2<T>(value: T): T { // T: type variable
return value;
}
let num func2(10); // num: number
let bool = func2(true); // bool: boolean
let str = func2("string"); // str: string
let arr = func<[number, number, number]>([1, 2, 3]); // 타입 변수를 튜플 타입으로 할당
function swap<T, U>(a: T, b: U) {
return [b, a];
}
const [a, b] = swap("1", 2); // T: string, U: number
function returnFirstValue<T>(a: [T, ...unknown[]]) { // 튜플
return a[0];
}
let num = returnFirstValue([0, 1, 2]); // num: number
// 0
let str = returnFirstValue(["hello", "mynameis"]); // str: string
// hello
let num2 = returnFirstValue([1, "hello", "mynameis"]); // num2: number
// 1
function getLength<T extends { length: number }>(data: T) { // length 프로퍼티를 필수로 만듭니다.
return data.length;
}
let var1 = getLength([1, 2, 3]); // 3
let var2 = getLength("12345"); // 5
let var3 = getLength({ length: 10 }); // 10
let var4 = getLength(10); // ❌ No
배열 메서드인 map()
과 forEach()
는 다양한 타입의 요소를 가진 배열에 모두 사용할 수 있으므로, 이를 타입스크립트에서 제네릭으로 표현할 수 있습니다.
map
메서드는 배열의 모든 요소에 대해 주어진 함수를 호출하고, 그 결과로 새로운 배열을 생성합니다. 이 때 각 요소의 타입은 제네릭을 사용하여 지정할 수 있습니다.
function map<T, U>(arr: T[], callback: (item: T) => U) {
let result = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i]));
}
return result;
}
const arr = [1, 2, 3];
map(arr, (it) => it * 2); // it: number
map(['hi', 'hello'], (it) => it.toUpperCase()); // it: string
forEach
메서드는 배열의 모든 요소에 대해 주어진 함수를 실행합니다. 이 때 각 요소의 타입은 제네릭을 사용하여 지정할 수 있습니다.
function forEach<T>(arr: T[], callback: (item: T) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i]);
}
}
const arr = [1, 2, 3];
forEach(arr, (it) => {
console.log(it.toFixed());
});
forEach(['123', '456'], (it) => { return it; }) // it: string
제네릭을 인터페이스에 적용하면, 인터페이스에 사용되는 특정 타입을 유연하게 정의할 수 있습니다.
interface KeyPair<K, V> {
key: K;
value: V;
}
let keyPair: KeyPair<string, number> = { // generic function과 달리 타입 변수에 타입을 할당해야 합니다.
key: "key",
value: 0,
};
let Keypair2: KeyPair<boolean, string[]> = {
key: true,
value: ["1"],
}
인덱스 시그니처를 사용하면, 어떤 키의 이름으로든 값을 저장할 수 있는 객체를 표현할 수 있습니다.
interface NumberMap {
[key: string]: number;
]
let numberMap1: numberMap = {
key: -1231,
key2: 123123,
};
interface Map<V> { // value의 타입을 타입 변수로 할당
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
}
타입 별칭(type alias)은 특정 타입이나 인터페이스에 이름을 붙이는 것을 말합니다. 타입 별칭은 복잡한 타입을 더 간결하게 표현할 수 있게 해주며, 제네릭을 적용하면 더욱 유연한 타입 표현이 가능해집니다.
type Map<V> = {
[key: string]: V;
};
let stringMap2: Map<string> = {
key: "hello",
}
interface Student {
type: "student";
school: string;
}
interface Develop {
type: "developer";
skill: string;
}
interface User<T> {
name: string;
profile: T;
}
function goToSchool(user: User<Student>) {
const school = user.profile.school;
console.log(`${school}로 등교 완료`);
}
const developerUser: User<Developer> = {
name: "이정환",
profile: {
type: "developer",
skill: "TypeScript",
},
};
const studentUser: User<Student> = {
name: "홍길동",
profile: {
type: "student",
school: "가톨릭대학교",
},
};
제네릭을 사용하면 클래스에서 작동하는 타입을 선언 시점이 아닌 인스턴스화 시점에서 결정할 수 있습니다.
// 일반 클래스
class NumberList {
constructor(private list: mnumber[]) {}
push(data: number) {
this.list.push(data);
}
pop() {
return this.list,pop();
}
print() {
console.log(this.list);
}
}
const numberList = new NumberList([1, 2, 3]);
numberList.pop();
numberList.push(4);
numberList.print();
// 제네릭 클래스
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 List1 = new List([1, 2, 3]); // List<number>를 안써도 됩니다.
List1.pop();
List1.push(4);
List1.print();
const List2 = new List(["1", "2");
List2.push("hello");
프로미스(Promise)는 자바스크립트에서 비동기 처리를 간편하게 할 수 있도록 설계된 객체입니다. 이는 비동기 작업이 완료된 이후의 결과값을 나타냅니다. 타입스크립트에서는 제네릭을 사용하여 프로미스가 반환하는 결과값의 타입을 지정할 수 있습니다.
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
resolve(20);
// reject("");
}, 3000);
});
promise.then((res)=>{
console.log(res * 10); // 200
});
promise.catch((err) => {
if (typeof err === "string") { // err 객체는 타입 좁히기를 사용해야 합니다.
console.error(err);
}
});
위의 예제에서 new Promise<number>
는 Promise<number>
타입의 프로미스를 생성합니다. 이 프로미스는 숫자 값을 반환합니다. then
메서드는 이 프로미스가 성공적으로 완료되었을 때 호출됩니다. 여기서 res
의 타입은 number
입니다.
프로미스를 반환하는 함수를 선언할 때도 반환 타입을 Promise<T>
형태로 선언할 수 있습니다.
interface Post {
id: number;
title: string;
content: string;
}
function fetchPost(): Promise<Post> { // 첫 번째 방법 (권장)
return new Promise<Post>((resolve, reject) => { // 두 번째 방법
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 컨텐츠",
});
}, 3000);
});
}
const postRequest = fetchPost();
postRequest.then((post) => { // post: Post
post.id;
});
Reference