II. 제네릭 타입 (Generic Types)
제네릭은 선언 시점이 아닌 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법입니다.
제네릭을 선언할 때 관용적으로 사용되는 대표적인 식별자로 T가 있고, 그 외에 U와 V가 있습니다. 반드시 T, U, V를 사용하여 하는 것은 아니지만 관용적인 식별자를 쓰는게 모범적입니다.
어떻게 보면 어떤 타입을 전달해도 사용이 가능한 any랑 다른점이 있을지 고민이 될수도 있지만 any는 타입체크를 전혀 하지 않아서 전달받은 데이터의 타입을 알 수 없고 반환할 때 타입의 정보를 반환하지 않습니다. 이런 반면에 제네릭은 전달받은 타입을 확인 및 반환을 할 수 있고 타입을 제한 할 수도 있습니다.
객체
제네릭 타입을 객체에 적용 하는데에 특정한 문법이 있습니다.
interface MyInterface<GenericValue> {
value: GenericValue;
}
const stringObject: MyInterface<string> = { value: "hello, world!" };
const numberObject: MyInterface<number> = { value: 1234 };
const stringArrayObject: MyInterface<Array<string>> = { // MyInterface<string[]> 기능적으로는 동일
value: ["hello", "world!"],
};
이렇게 우리가 함수에 인자를 보내주는 것과 같이 인터페이스 및 타입에도 제네릭 타입을 적용하여 타입의 프로퍼티에 보다 유연하게 타입을 입힐 수 있습니다.
제네릭 타입에 기초값을 지정할수도 있는데, 기초값이 string으로 지정을 해서 stringObject 상수의 타입인 MyInterface에 <string>을 명시해주지 않아도 됩니다.
interface MyInterface<GenericValue = string> {
value: GenericValue;
}
const stringObject: MyInterface = { value: "hello, world!" }; // ✅
const myRandomObject: MyInterface = { value: true }; // ❌ Type 'boolean' is not assignable to type 'string'.
함수
함수에는 제네릭 타입이 어떻게 적용이 되는지 살펴보겠습니다.
type User = {
email: string;
name: string;
};
function getData<T>(data: T): T {
return data;
}
// 에러 없이 콘솔로그 되는 유효한 호출
console.log(getData<string>("string data"));
console.log(getData<number>(1234));
console.log(getData<User>({ email: "email@email.com", name: "katie" }));
console.log(getData<string[]>(["string", "data"]));
console.log(getData<string[]>([])); // 빈 배열도 유효한 인자입니다!
이렇게 함수를 선언할 때 제네릭을 적용하는 방법을 익히는것도 중요하지만,
타입스크립트에서 우리가 자주 사용하는 자바스크립트 메소드가 어떤 타입을 기대하고,
또 어떤 타입을 반환하고 있는지 봅니다.
먼저 Object.keys() 를 살펴보겠습니다.
enum Status {
Initiated = "Initiated",
Pending = "Pending",
Shipped = "Shipped",
Delivered = "Delivered",
}
interface Order {
buyer: string;
orderStatus: Status;
}
// ❌ Type 'string' is not assignable to type 'Status'.
const orders: Order[] = Object.keys(Status).map((status, index) => {
return {
buyer: `buyer #${index}`,
orderStatus: status,
};
});
Status라는 enum이 있는데, enum을 Object.keys()를 적용해 Order가 배열이 값으로 담겨져있는 결과를 얻고싶은데, 에러가 납니다. 문자열은 Status라는 타입에 할당이 불가능하다고 나오고, 우리는 Enum 강의에서 잠깐 봤던 Object.keys()의 시그니처를 다시 한번 살펴보겠습니다.
keys(o: object): string[];
Status라는 enum을 Object.keys()안에 매개변수로 보내면 Status라는 타입은 string으로 전환이 되어버립니다. 이러한 이유로 as 키워드로 타입캐스팅을 해줄수도 있지만, 그것보다 더 안전한 방법이 있습니다.
const orders: Order[] = Object.values(Status).map((status, index) => {
return {
buyer: `buyer #${index}`,
orderStatus: status,
};
});
간단하게 Object.keys() 를 Object.values()로 바꿔쓰면 되는데요, 왜 그런지 알아볼게요. 먼저 Object.values()의 함수 시그니처를 보면 제네릭 함수를 사용해서 우리가 인자로 보낸 enum의 타입을 존중 해 주는걸 확인할 수 있습니다.
values<T>(o: { [s: string]: T } | ArrayLike<T>): T[];
제네릭 T가 Status가 되고, 곧 T, 즉 Status가 배열에 담겨 반환이 되는거죠! 그래서 타입캐스팅 없이 타입을 존중해주는 Object.values()를 쓰시는 것 꼭 기억하세요!
🥊 챌린지: Object.entries() 함수 시그니처는 어떻게 구성이 되어있는지 확인해보세요
클래스
클래스에는 제네릭이 어떻게 적용이 되는지 보겠습니다. stack 데이터 구조를 클래스와 인터페이스를 이용하여 구현해 보는게 좋을 것 같아요.
interface IStack<T> {
push(item: T): void;
pop(): T | undefined;
peek(): T | undefined;
size(): number;
}
먼저 Stack이라는 클래스에 속성해 있어야 될 주요 함수들을 인터페이스로 정의를 합니다. 그 다음에 클래스를 본격적으로 써볼건데요, 이렇게 먼저 인터페이스를 선언 하고 클래스에 implements 를 해주면 인스턴스 함수들이 오토컴플릿이 되는걸 목격할 수 있습니다.
class Stack<T> implements IStack<T> {
private storage: T[] = [];
constructor(private capacity = 4) {}
push(item: T): void {
if (this.size() === this.capacity) {
throw Error("stack is full");
}
this.storage.push(item);
}
pop(): T | undefined {
return this.storage.pop();
}
peek(): T | undefined {
return this.storage[this.size() - 1];
}
size(): number {
return this.storage.length;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
numberStack.push(100);
console.log(numberStack.peek()); // 100
console.log(numberStack.size()); // 4
numberStack.push(101); // ❌ Error: stack is full
push(item: T): void 함수는 storage 배열에 인자로 받는 item값을 담아주는 함수입니다. 반환하는 값을 받고싶지 않아서 반환 타입을 void라고 설정을 했습니다. 추가로 capacity라는 private 클래스 상수를 새로운 스택이 형성 될때마다 선언이 되도록 했는데요, push를 했을 때 스택이 수용할 수 있는 제한을 둔겁니다. 그래서 우리가 101을 담으려고 하면 stack is full 이라는 에러를 받습니다. capacity를 설정하지 않고 class를 설립할 수 있으니 원하시면 private capacity = 4를 지우시면 됩니다.
pop() :T | undefined 는 storage 배열의 제일 마지막 ㄷ에 위치한 값을 뽑아내는거죠. undefined는 storage가 비어있을 때를 위해 반환값이므로 설정해줍니다.
peek(): T | undefined도 pop()과 마찬가지로 storage 배열의 제일 마지막 index에 위치한 값을 다루는 함수인데요, storage 배열이 비어있을수도 있으니 대비해서 undefined를 반환값으로 T와 유니언 타입으로 설정해줍니다.
마지막으로 size(): number 함수는 storage의 배열의 길이를 반환합니다, 즉 숫자를 반환하는 것이죠.
'코딩캠프 > 내일배움캠프' 카테고리의 다른 글
[ WIL ] 01.25~27 11주차 (0) | 2023.01.29 |
---|---|
[ TIL ] 01.27(금) 53일차 (0) | 2023.01.27 |
[ TIL ] 01.25(수) 51일차 (0) | 2023.01.25 |
[ WIL ] 01.16~20 10주차 (0) | 2023.01.22 |
[ TIL ] 01.20(금) 50일차 (0) | 2023.01.20 |