💡 제네릭 인터페이스
• 인터페이스에 제네릭 적용하기
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"],
};
변수 keyPair의 타입으로 KeyPair<string, number>를 정의했습니다. 그 결과 K에는 string, V에는 number 타입이 각각 할당되어 key 프로퍼티는 string 타입이고, value 프로퍼티에는 number 타입인 객체 타입이 됩니다.
변수 KeyPair2의 타입으로 KeyPair<boolean, string[ ]>를 정의했습니다. 글 결과 K에는 boolean, V에는 string[ ] 타입이 각각 할당되어 key 프로퍼티는 boolean 타입이고 value 프로퍼티는 string[ ] 타입인 객체 타입이 됩니다.
이때 주의해야할 점은 제네릭 인터페이스는 제네릭 함수와는 달리 변수의 타입으로 정의할 때 반드시 꺽쇠와 함께 타입 변수에 할당할 타입을 명시해주어야 합니다. 왜냐하면 제네릭 함수는 매개변수에 제공되는 값의 타입을 기준으로 타입 변수의 타입을 추론할 수 있지만 인터페이스는 마땅히 추론할 수 있는 값이 없기 때문입니다.
• 인덱스 시그니쳐와 함께 사용하기
제네릭 인터페이스는 인덱스 시그니쳐와 함께 사용하면 기존보다 훨씬 더 유연한 객체 타입을 정의할 수 있습니다.
interface Map<V> {
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
한개의 타입 변수 V를 갖는 제네릭 인터페이스 Map을 정의하였습니다. 이 인터페이스는 인덱스 시그니쳐로, key의 타입은 string, value의 타입은 V인 모든 객체 타입을 포함하는 타입입니다.
변수 stringMap의 타입을 Map<string>으로 정의하였습니다. 따라서 V가 string 타입이 되어 이 변수의 타입은 key는 string이고 value는 string인 모든 프로퍼티를 포함하는 객체 타입으로 정의됩니다.
변수 booleanMap의 타입을 Map<boolean>으로 정의하였습니다. 따라서 V가 boolean 타입이 되어 이 변수의 타입은 key는 string이고 value는 boolean인 모든 프로퍼티를 포함하는 객체 타입으로 정의됩니다.
• 제네릭 타입 별칭
// 제네릭 타입 별칭
type Map2<V> = {
[key: string]: V;
};
let stringMap2: Map2<string> = {
key: "hello",
};
인터페이스와 마찬가지로 타입 별칭에도 역시 제네릭을 적용할 수 있습니다. 제네릭 타입 별칭을 사용할 떄에도 제네릭 인터페이스와 마찬가지로 타입으로 정의할 때 반드시 타입 변수에 설정할 타입을 명시해 주어야 합니다.
• 제네릭 인터페이스의 활용 예시
다음과 같이 학생 유저와 개발자 유저가 있는 유저 관리 프로그램이 있다고 가정해봅시다.
// ⓵
interface Student {
type: "student";
school: string;
}
interface Developer {
type: "developer";
skill: string;
}
// ⓶
interface User {
name: string;
profile: Student | Developer;
}
// ⓷
function goToSchool(user: User<Student>) {
if (user.profile.type !== "student") {
console.log("잘못된 경로입니다");
return;
}
const school = user.profile.school;
console.log(`${school}로 등교 완료!`);
}
const developerUser: User = {
name: "수현",
profile: {
type: "developer",
skill: "typescript",
},
};
const studentUser: User = {
name: "해린",
profile: {
type: "student",
school: "뉴진스학교",
},
};
⓵ 학생을 의미하는 Student와 개발자를 의미하는 Developer 타입을 정의하였습니다. 두 타입 모두 String Literal 타입의 type 프로퍼티를 가지고 있으며, 서로소 유니온 타입입니다.
⓶ 그리고 학생일 수도 있고 개발자일 수도 있는 User 타입을 정의하였습니다. 특정 객체가 학생이라면 profile 프로퍼티에 Student 타입의 객체가 저장될 것이고, 그렇지 않으면 Developer 타입의 객체가 저장될 것입니다.
⓷ 그 아래에는 학생 유저만 이용할 수 있는 함수 goToSchool을 선언하였고, User 타입의 객체를 받아 타입을 좁혀서 이 유저가 학생일 경우에만 등교를 완료하였다는 내용을 콘솔에 출력합니다.
이 코드에는 당장의 문제가 없습니다. 하지만 점점 학생만이 할 수 있는 기능이 많아진다고 하면 매번 기능을 만들기 위해 함수를 선언할 떄마다 조건문을 이용하여 타입을 좁혀야하는 불편함이 있습니다. 이때 타입을 좁히는 코드는 중복 코드가 될 것입니다. 이럴 때 바로 제네릭 인터페이스를 이용하면 좋습니다. 다음과 같이 User 인터페이스를 제네릭 인터페이스로 수정합니다.
interface Student {
type: "student";
school: string;
}
interface Developer {
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: "인덕대학교",
},
};
이렇게 변경하면 goToSchool 함수의 매개변수 타입을 User<Student> 처럼 정의하여 학생 유저만 이 함수의 인수를 전달하도록 제한할 수 있습니다. 결과적으로 함수 내부에서 타입을 좁힐 필요가 없어지므로 코드가 훨씬 간결해질 것입니다.
💡 제네릭 클래스
class NumberList {
constructor(private list: number[]) {}
push(data: number) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
const numberList = new NumberList([1, 2, 3]);
다음은 Number 타입의 리스트를 생성하는 클래스입니다. list 필드를 private 접근 제어자로 설정하여 클래스 내부에만 접근할 수 있도록 만들고, 생성자에서 필드 선언과 함께 초기화합니다. 새로운 요소를 추가하는 push, 제거하는 pop, 출력하는 print 메서드도 함께 만들어주었습니다.
class StringList {
constructor(private list: string[]) {}
push(data: string) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
const numberList = new StringList(["1", "2", "3"]);
그런데 만약 StringList 클래스도 하나 더 필요한 경우라면 어떻게 해야할까요? 제네릭 없이는 다음과 같이 새로운 클래스를 하나 더 만들어주어야합니다. 하지만 이것도 매우 비효율적입니다. 모든 리스트에 새롭게 추가되거나 수정되는 내용이 있다면 모두 다 변경하는 번거로움이 있기 떄문입니다. 따라서 이러한 경우에는 제네릭 클래스를 사용하여 여러 타입의 리스트를 생성할 수 있는 범용적인 클래스를 정의하면 됩니다.
// 제네릭 클래스
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"]);
클래스의 이름 뒤에 타입 변수를 선언하면 제네릭 클래스가 됩니다. 이 타입 변수는 이제 클래스 내부에서 자유롭게 사용할 수 있습니다. 또한, 클래스는 생성자를 통해 타입 변수의 타입을 추론할 수 있기 때문에 생성자에 인수로 전달하는 값이 있을 경우 타입 변수에 할당할 타입을 생략해도 됩니다.
const numberList = new List<number>([1, 2, 3]);
const stringList = new List<string>(["1", "2"]);
만약, 타입변수의 타입을 직접 설정하고 싶다면 다음과 같이 작성해주면 됩니다.
💡 프로미스와 제네릭
Promise는 제네릭 클래스로 구현되어 있습니다. 따라서 새로운 Promise를 생성할 때 다음과 같이 타입 변수에 할당할 타입을 직접 설정해주면 해당 타입이 바로 resolve 결과값의 타입이 됩니다.
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
resolve(20);
}, 3000);
});
promise.then((response) => {
// response는 number 타입
console.log(response);
});
promise.catch((error) => {
if (typeof error === "string") {
console.log(error);
}
});
하지만 reject 함수에 인수로 전달하는 값인 실패의 결과값 타입은 정의할 수 없습니다. 기본값으로 unknown 타입이 고정되어 있기 때문에 catch 메서드에서 사용하려면 타입 좁히기를 통해 사용하기를 권장합니다.
function fetchPost() {
return new Promise<Post>((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 본문",
});
}, 3000);
});
}
만약 어떤 함수가 Promise 객체를 반환한다면 함수의 반환값 타입을 이렇게 지정할 수 있습니다.
function fetchPost(): Promise<Post> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 컨텐츠",
});
}, 3000);
});
}
또는 더 직관적으로 다음과 같이 반환값 타입을 직접 명시하여도 됩니다.
출처) 한 입 크기로 잘라먹는 타입스크립트(TypeScript)_이정환
'📍 프로그래밍 언어 > TypeScript' 카테고리의 다른 글
[ TypeScript ] 타입 조작하기 2 - 맵드 타입, 템플릿 리터럴 타입 (0) | 2024.12.23 |
---|---|
[ TypeScript ] 타입 조작하기 1 - 인덱스드 액세스 타입, keyof & typeof 연산자 (0) | 2024.12.23 |
[ TypeScript ] 제네릭 이해하기: 타입 변수와 메서드 타입 정의(map, forEach) (1) | 2024.12.22 |
[ TypeScript ] 클래스 이해하기: JavaScript 차이점, 인터페이스, 그리고 접근 제어자 (0) | 2024.12.22 |
[ TypeScript ] 인터페이스의 활용: 기본 개념부터 확장과 선언 합치기 (0) | 2024.12.21 |