제네릭 인터페이스, 타입 별칭, 그리고 활용 예시
**제네릭(Generic)**은 타입을 일반화하여 여러 타입에 대해 유연하게 작동할 수 있는 코드를 작성하는 강력한 도구입니다. 이를 인터페이스, 타입 별칭, 그리고 실용적인 예시에 적용해 봅시다.
1. 제네릭 인터페이스
제네릭 인터페이스는 특정 인터페이스가 여러 타입을 받을 수 있도록 만들어 주는 기능입니다. 예를 들어, **KeyPair**라는 제네릭 인터페이스를 정의하여 키와 값을 서로 다른 타입으로 설정할 수 있습니다.
interface KeyPair<K, V> {
key: K;
value: V;
}
여기서 **K**와 **V**는 각각 key와 value의 타입을 정의하는 타입 변수입니다.
- 사용 예:
-
let keyPair: KeyPair<string, number> = { key: "key", value: 0, }; let keyPair2: KeyPair<boolean, string[]> = { key: true, value: ["1"], };
- keyPair의 타입은 KeyPair<string, number>로 설정되어, key는 string 타입이고 value는 number 타입이 됩니다.
- keyPair2의 타입은 KeyPair<boolean, string[]>로 설정되어, key는 boolean 타입이고 value는 string[] 타입입니다.
주의점: 제네릭 인터페이스를 사용할 때는 항상 타입 변수를 명시해 주어야 합니다. 이는 함수와 달리 인터페이스는 타입을 추론할 기준이 없기 때문입니다.
2. 인덱스 시그니처와 제네릭
인덱스 시그니처와 제네릭을 함께 사용하면 객체 타입을 더 유연하게 정의할 수 있습니다.
interface Map<V> {
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
- 여기서 Map<V> 인터페이스는 [key: string]: V 형태로 정의되어, 모든 키는 문자열이고 **값은 타입 변수 V**에 의해 결정됩니다.
- stringMap은 Map<string>으로 정의되어 값이 string 타입인 객체를 뜻합니다.
- booleanMap은 Map<boolean>으로 정의되어 값이 boolean 타입인 객체를 뜻합니다.
이렇게 하면 어떤 키-값 쌍이든 유연하게 표현할 수 있는 객체 타입을 정의할 수 있습니다.
3. 제네릭 타입 별칭
타입 별칭에도 제네릭을 적용할 수 있습니다. 타입 별칭을 사용하면 타입에 별명을 붙여서 재사용할 수 있습니다.
type Map2<V> = {
[key: string]: V;
};
let stringMap2: Map2<string> = {
key: "string",
};
여기서 **Map2<V>**는 Map 인터페이스와 같은 형태로 인덱스 시그니처를 포함합니다. 값의 타입을 지정하면 그 타입으로 객체를 정의할 수 있습니다.
4. 제네릭 인터페이스 활용 예시
아래는 제네릭 인터페이스의 실용적인 활용 예시입니다. **학생(Student)**과 개발자(Developer) 타입을 정의하고 이를 **사용자(User)**에 연결해봅니다.
1) 학생과 개발자 타입 정의
interface Student {
type: "student";
school: string;
}
interface Developer {
type: "developer";
skill: string;
}
- Student는 학생을 나타내며 type과 school이라는 속성을 가집니다.
- Developer는 개발자를 나타내며 type과 skill 속성을 가집니다.
2) User 타입에 제네릭 적용하기
제네릭을 이용해 User 타입을 유연하게 정의합니다.
interface User<T> {
name: string;
profile: T;
}
- User<T>는 제네릭 인터페이스로, T에 학생 또는 개발자 같은 타입을 받을 수 있습니다.
3) 사용자 정의 및 함수 활용
학생과 개발자 객체를 정의하고, 특정 사용자만 사용할 수 있는 함수를 만듭니다.
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(studentUser); // "가톨릭대학교로 등교 완료"
- goToSchool 함수는 User<Student> 타입의 객체만 받을 수 있도록 정의되어 있습니다.
- 이를 통해 학생 유저만 이 함수의 인수로 전달할 수 있어, 타입 안전성을 높이고 불필요한 조건문을 줄일 수 있습니다.
요약
- 제네릭 인터페이스:
- 인터페이스에 제네릭을 적용하여 여러 타입을 지원하는 인터페이스를 정의할 수 있습니다.
- 예: interface KeyPair<K, V> { key: K; value: V; }
- 인덱스 시그니처와 제네릭:
- 인덱스 시그니처와 제네릭을 결합하여 유연한 객체 타입을 정의할 수 있습니다.
- 예: interface Map<V> { [key: string]: V; }
- 제네릭 타입 별칭:
- 타입 별칭에도 제네릭을 적용하여 유연하게 사용할 수 있습니다.
- 예: type Map2<V> = { [key: string]: V; }
- 제네릭 인터페이스 활용:
- **유저(User)**와 같은 인터페이스에 제네릭을 사용하면 특정 타입의 사용자(학생 또는 개발자 등)에 대해 타입 안전성을 높일 수 있습니다.
- 특정 유저 타입만 인수로 받는 함수에서 타입을 좁히는 작업 없이 직접 활용할 수 있습니다.
제네릭을 사용하면 타입의 유연성과 코드의 재사용성을 동시에 가져갈 수 있습니다. 특히, 제네릭 인터페이스를 활용하면 코드의 안정성이 강화되고 중복 코드를 줄일 수 있어, 실용적이고 효율적인 코드를 작성할 수 있습니다.
제네릭 클래스 (Generic Class)
제네릭 클래스는 다양한 타입에 대해 하나의 클래스로 작업을 수행할 수 있도록 만든 범용적인 클래스입니다. 이를 통해 중복 코드를 줄이고, 코드의 재사용성과 유연성을 높일 수 있습니다. 아래에서는 제네릭 클래스의 개념과 그 적용 방법을 살펴보겠습니다.
1. 제네릭이 없는 클래스의 문제점
먼저, 제네릭이 적용되지 않은 NumberList 클래스를 정의해 봅니다.
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]);
- NumberList 클래스는 list 필드를 가지고 있으며, 이를 통해 숫자 요소를 추가(push), 제거(pop), 출력(print)할 수 있습니다.
- list 필드는 private으로 설정되어 있어, 클래스 내부에서만 접근이 가능합니다.
이제, 같은 방식으로 **문자열 리스트(StringList)**를 필요로 한다면 새로운 클래스를 정의해야 합니다.
class StringList {
constructor(private list: string[]) {}
push(data: string) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
const stringList = new StringList(["1", "2", "3"]);
- NumberList와 StringList 클래스는 거의 동일한 구조를 가집니다.
- 다른 타입의 리스트를 만들기 위해 중복 코드를 작성해야 하므로, 유지보수가 어렵고 효율적이지 않다는 문제가 발생합니다.
2. 제네릭 클래스 사용하기
위의 중복 문제를 해결하기 위해 제네릭 클래스를 사용할 수 있습니다. 제네릭을 사용하면 하나의 클래스로 여러 타입의 리스트를 처리할 수 있게 됩니다.
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"]);
- 제네릭 클래스 선언:
- 클래스 이름 뒤에 **<T>**를 사용해 **타입 변수 T**를 선언합니다.
- 이제 클래스 내부에서 **T**를 사용할 수 있으며, 리스트의 타입을 T로 일반화했습니다.
- 따라서 **T**는 숫자, 문자열, 기타 다른 타입으로 사용할 수 있습니다.
- 사용 예:
- new List([1, 2, 3]): **T**는 **number**로 추론됩니다.
- new List(["1", "2"]): **T**는 **string**으로 추론됩니다.
3. 타입 직접 명시하기
타입스크립트는 일반적으로 생성자에 전달된 값을 기준으로 타입을 추론하지만, 명시적으로 타입을 지정할 수도 있습니다.
const numberList = new List<number>([1, 2, 3]);
const stringList = new List<string>(["1", "2"]);
- <number>, **<string>**과 같이 타입을 직접 명시할 수 있습니다.
- 이렇게 하면 타입 안정성이 더욱 명확해지며, 타입 오류를 방지할 수 있습니다.
4. 제네릭 클래스의 장점 요약
- 타입의 유연성:
- 제네릭 클래스를 사용하면 다양한 타입의 리스트를 하나의 클래스로 처리할 수 있습니다. 숫자, 문자열, 객체 등 여러 타입을 처리할 수 있어 유연성이 높아집니다.
- 중복 코드 제거:
- 같은 기능을 하는 클래스를 타입만 다르게 여러 개 작성할 필요가 없어지므로 중복 코드가 제거됩니다. 이는 유지보수에 있어서도 큰 장점입니다.
- 타입 안정성:
- 제네릭을 사용함으로써 타입스크립트의 타입 추론 기능을 그대로 사용할 수 있어 타입 안정성을 보장합니다. 이를 통해 잘못된 타입의 데이터를 처리하는 오류를 줄일 수 있습니다.
5. 제네릭 클래스 사용 예시
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<number>([1, 2, 3]); // number 타입 리스트
numberList.push(4);
numberList.print(); // 출력: [1, 2, 3, 4]
const stringList = new List<string>(["a", "b", "c"]); // string 타입 리스트
stringList.push("d");
stringList.print(); // 출력: ["a", "b", "c", "d"]
- 숫자 리스트 (numberList)와 문자열 리스트 (stringList)를 하나의 제네릭 클래스로 관리할 수 있습니다.
- push, pop, print 등의 메서드가 타입에 맞게 동작하며, 타입 오류를 방지할 수 있습니다.
요약
- 제네릭 클래스는 다양한 타입을 하나의 클래스로 처리할 수 있도록 해 주는 강력한 기능입니다.
- 제네릭을 사용하면 중복 코드가 제거되고, 유지보수가 쉬운 코드를 작성할 수 있습니다.
- 클래스의 이름 뒤에 **타입 변수 T**를 선언하여, 클래스 내부에서 모든 타입 관련 작업을 T로 일반화할 수 있습니다.
- 제네릭 클래스를 사용하면 코드의 유연성, 재사용성, 타입 안전성이 모두 향상됩니다.
제네릭 클래스를 사용함으로써, 코드의 유연성과 안정성을 모두 확보할 수 있으며, 이를 통해 유지보수하기 쉬운 코드 구조를 만들 수 있습니다. 특히 리스트와 같은 데이터 구조를 구현할 때 매우 유용하며, 여러 타입을 다루는 작업에서도 복잡성을 줄일 수 있는 장점이 있습니다.
Promise 사용하기
Promise는 비동기 작업의 완료나 실패를 나타내는 객체입니다. Promise는 타입스크립트에서 제네릭(Generic) 클래스로 구현되어 있으며, 이를 통해 resolve 결과값의 타입을 명확히 정의할 수 있습니다.
아래에서는 Promise의 기본 사용법과 타입 정의에 대해 살펴보겠습니다.
1. Promise 기본 사용법
Promise는 비동기 작업이 성공했을 때 호출되는 **resolve**와, 실패했을 때 호출되는 **reject**를 사용하여 결과를 처리합니다. 제네릭 타입을 사용해 resolve 값의 타입을 명확하게 설정할 수 있습니다.
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
// 결과값: 20
resolve(20);
}, 3000);
});
promise.then((response) => {
// response는 number 타입으로 추론됨
console.log(response); // 출력: 20
});
promise.catch((error) => {
if (typeof error === "string") {
console.log(error);
}
});
- 제네릭 타입 설정:
- **new Promise<number>**를 사용해 number 타입의 결과값을 가진 Promise를 생성합니다.
- **resolve(20)**은 성공 시 반환될 값이며, 이 값의 타입은 **number**입니다.
- then 메서드:
- 비동기 작업이 성공했을 때 호출됩니다.
- response는 **resolve**에서 넘겨받은 **20**으로, 타입스크립트는 이를 **number**로 추론합니다.
- catch 메서드:
- 비동기 작업이 실패했을 때 호출됩니다.
- reject 함수의 인수로 전달된 실패 이유의 타입은 기본적으로 **unknown**입니다.
- 따라서 안전하게 사용하려면 타입 좁히기(typeof)를 통해 타입을 확인하고 사용해야 합니다.
주의사항: reject 함수로 전달하는 실패 이유의 타입은 타입 변수로 설정할 수 없고, 기본적으로 unknown 타입입니다. 따라서 실패 값을 사용할 때는 항상 타입을 좁혀서 사용해야 합니다.
2. Promise를 반환하는 함수
Promise를 반환하는 함수에서는 반환값의 타입을 명확하게 지정할 수 있습니다. 이를 통해 함수가 반환하는 Promise의 resolve 타입을 명확히 정의하고 타입 안정성을 유지할 수 있습니다.
interface Post {
id: number;
title: string;
content: string;
}
// 방법 1: Promise 생성 시 타입 지정
function fetchPost() {
return new Promise<Post>((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 본문",
});
}, 3000);
});
}
- fetchPost 함수는 비동기 작업을 시뮬레이션하기 위해 Promise 객체를 반환합니다.
- Post 인터페이스는 게시글 데이터를 나타내며, id, title, content의 속성을 가집니다.
- **resolve**를 통해 Post 타입의 객체를 반환합니다.
function fetchPost(): Promise<Post> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
title: "게시글 제목",
content: "게시글 본문",
});
}, 3000);
});
}
- 반환 타입 명시:
- 함수의 반환 타입을 **Promise<Post>**로 명시합니다.
- 이 경우, 타입스크립트는 함수가 Post 타입의 값을 반환하는 Promise임을 인식합니다.
- 이 방식으로 반환 타입을 명확히 정의하면, 함수가 반환하는 Promise의 resolve 타입을 더욱 명확하게 나타낼 수 있어 타입 안정성이 향상됩니다.
요약
- Promise의 제네릭 타입:
- **new Promise<number>**와 같이 제네릭 타입을 사용하여 Promise의 resolve 값의 타입을 명확하게 정의할 수 있습니다.
- 이를 통해 then 메서드에서 사용되는 값의 타입이 자동으로 추론되므로 타입 안전성을 유지할 수 있습니다.
- reject 값의 타입:
- reject 함수에 전달되는 실패 이유의 타입은 **unknown**으로 고정됩니다.
- 따라서 catch 메서드에서 이 값을 사용하려면 타입 좁히기(typeof 등을 사용)를 통해 안전하게 처리하는 것이 좋습니다.
- Promise를 반환하는 함수의 타입 명시:
- 비동기 작업을 수행하는 함수가 Promise 객체를 반환하는 경우, 함수의 반환 타입을 명확하게 명시하는 것이 좋습니다.
- 예를 들어, **function fetchPost(): Promise<Post>**와 같이 반환 타입을 명확히 정의하면 타입 안정성이 향상됩니다.
실용적인 예시와 장점
- 비동기 작업 타입 안정성: 제네릭을 사용하여 Promise의 결과값의 타입을 명확히 지정하면, 타입스크립트의 타입 추론 기능을 활용하여 비동기 작업의 타입 안전성을 보장할 수 있습니다.
- 코드의 가독성과 유지보수성: 반환 타입을 명확히 지정하면 코드의 가독성이 높아지고, 유지보수 시 실수할 가능성이 줄어듭니다.
이렇게 Promise를 제네릭과 함께 사용하면 비동기 작업의 타입 안정성을 유지하면서 다양한 비동기 처리를 안전하게 관리할 수 있습니다. 이는 코드의 가독성을 높이고, 오류를 사전에 방지하는 데 매우 유용합니다.
'TypeScript' 카테고리의 다른 글
TypeScript 조건부 타입 (1) | 2024.11.03 |
---|---|
TypeScript 타입조작하기 (0) | 2024.11.02 |
TypeScript 제네릭1 (1) | 2024.10.29 |
TypeScript 인터페이스로 구현하는 클래스 (0) | 2024.10.29 |
TypeScript 클래스 (1) | 2024.10.29 |