제네릭
제네릭 소개
/**
* 제네릭
* = 일반적인
* 모든 타입에 두루두루 쓸 수 있는 범용 (함수)
*/
// 제네릭 함수
// 함수에 인수에 따라서 반환값의 타입을 가변적으로 정해줄 수 있다!
function func<T>(value: T): T {
return value;
}
let num = func(10);
let bool = func(true);
let str = func("string");
let arr = func<[number, number, number]>([1, 2, 3]);
// let arr = func<[number, number, number]>([1, 2, 3] as [number, number, number]);
타입 변수 응용하기
/**
* 첫 번째 사례
*/
function swap<T, U>(a: T, b: U) {
return [b, a];
}
const [a, b] = swap("1", 2);
/**
* 두 번째 사례
*/
// tuple type은 특정 인덱스에 해당하는 타입을 지정할 수 있음
// 첫 번째 요소 이후에 들어오는 요소는 무슨 타입인지, 몇 개인지 알 필요가 x -> ...unknown[]
function returnFirstValue<T>(data: [T, ...unknown[]]) {
return data[0];
}
let num = returnFirstValue([0, 1, 2]);
console.log(num);
let str = returnFirstValue([1, "hello", "myNameIs"]);
console.log(str);
/**
* 세 번째 사례
*/
// 아래 extends 의미와 똑같음
// interface InterfaceA {
// length: number;
// }
// interface InterfaceB extends InterfaceA {
// }
function getLength<T extends { length: number }>(data: T) {
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); X
map, forEach 메서드 타입 정의하기
/**
* map 메서드
*/
const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2); // [2, 4, 6]
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;
}
map(arr, (it) => it * 2); // [2, 4, 6]
map(["hi", "hello"], (it) => it.toUpperCase()); // [ 'HI', 'HELLO' ]
map(["hi", "hello"], (it) => parseInt(it)); // [ NaN, NaN ]
/**
* forEach
*/
const arr2 = [1, 2, 3];
arr2.forEach((it) => console.log(it));
function forEach<T>(arr: T[], callback: (item: T) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i]);
}
}
forEach(arr2, (it) => {
console.log(it.toFixed());
});
forEach(["123", "456"], (it) => {
it;
});
제네릭 인터페이스 & 제네릭 타입 별칭
/**
* 제네릭 인터페이스
*/
// 타입 변수 = 타입 파라미터 = 제네릭 타입 변수 = 제네릭 타입 파라미터
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 NumberMAP {
[key: string]: number;
}
let numberMap1: NumberMAP = {
key: -1231,
key2: 123123,
};
// 하나의 타입으로 다양한 객체 표현 가능
interface Map<V> {
[key: string]: V;
}
let stringMap: Map<string> = {
key: "value",
};
let booleanMap: Map<boolean> = {
key: true,
};
/**
* 제네릭 타입 별칭
*/
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<T> {
name: string;
profile: T;
}
function goToSchool(user: User<Student>) {
// 제네릭 인터페이스 이용하면 타입 좁히기 없어도 됨
// if (user.profile.type !== "student") {
// console.log("학생만 출입 가능합니다.");
// return;
// }
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: "MIT",
},
};
제네릭 클래스
/**
* 제네릭 클래스
*/
// NumberList에 다른 타입을 받는 List를 만들려면 중복이 일어남 -> 제네릭 클래스로 다양한 타입을 사용하는 List 정의할 수 있음
class List<T> {
constructor(private list: T[]) {}
push(data: T) {
this.list.push(data);
}
pop() {
return this.list.pop();
}
print() {
console.log(this.list);
}
}
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.pop();
numberList.push(4);
numberList.print(); // [1, 2, 4]
// 제네릭 클래스는 제네릭 인터페이스나 제네릭 타입 변수와 다르게 클래스의 생성자를 호출할 때 생성자의 인수로 전달하는 값을 기준으로 자동 타입 추론
const list = new List(["hi", 1, 2, true]);
list.print(); // [ 'hi', 1, 2, true ]
프로미스와 제네릭
/**
* 프로미스
*/
// 프로미스는 제네릭 클래스를 기반으로 타입이 선언되어 있기 때문에 타입 변수로 비동기 처리의 결괏값 타입을 정해줄 수 있지만
// reject 실패했을 때 타입은 정해줄 수 x
// 타입 변수 정하지 않으면 -> 기본 unknown
const promise = new Promise<number>((resolve, reject) => {
setTimeout(() => {
resolve(20);
reject("~ 때문에 실패");
}, 3000);
});
promise.then((response) => {
console.log(response * 10); // 20
});
promise.catch((err) => {
if (typeof err === "string") {
console.log(err);
}
});
/**
* 프로미스를 반환하는 함수의 타입을 정의
*/
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.id;
});
[참고]
한 입 크기로 잘라먹는 타입스크립트
- Section 7. 제네릭 강의를 듣고 정리한 내용입니다.