코딩/Typescript

타입스크립트 Generics in Class - 타입스크립트 TypeScript 강좌 13편

드리프트 2022. 3. 1. 20:30
728x170

안녕하세요?

 

오늘은 지난 시간에 배운 Class Visibility에 더해서 클래스에서 제네릭을 쓰는 방법에 대해 알아보겠습니다.

 

일단 지난 시간에 만들었던 Database 클래스를 <T, K>를 이용한 제네릭 형태로 변경해 보겠습니다.

 

interface Database {
    get(id: string): string;
    set(id: string, value: string): void;
}

class InMemoryDatabase implements Database {
    protected db: Record<string, string> = {};
    get(id: string): string {
        return this.db[id];
    }
    set(id: string, value: string): void {
        this.db[id] = value;
    }
}

위 코드를 보시면 InMemoryDatabase 클래스는 Database 인터페이스를 구현하는데요.

 

Database의 get 함수 부분의 리턴 타입인 string을 제네릭으로 바꿔 보겠습니다.

 

먼저, InMemoryDatabase<T> 형식으로 바꿨습니다.

 

그리고 db 변수의 두 번째도 T로 바꿨고 이에 해당되는 get 함수의 리턴 타입도 T로 바꿨고, set 함수의 value 인자의 타입도 T로 바꿨습니다.

 

위 스크린숏을 보시면 에러가 떴는데요. 왜냐하면 Database interface가 제네릭을 구현하지 않기 때문입니다.

 

이제 Database interface 부분을 제네릭으로 바꿔 볼까요?

 

Database interface 부분도 맞게 제네릭으로 변경했습니다.

 

그런데 밑에 있는 PersistentMemoryDB에서 에러가 나오고 있는데요.

 

당연한 결과고요. PersistentMemoryDB도 제네릭으로 바꿔야 합니다.

위와 같이 에러가 발생한 부분을 제네릭에 맞게 고쳐 보겠습니다.

PersistentMemoryDB 부분의 제네릭화는 간단합니다. <T>만 넣으면 됩니다.

 

그럼 이제 테스트해볼까요?

 

PersistentMemoryDB 객체를 myDB라는 이름으로 만들고 myDB.set 함수를 실행하려고 하니까 위와 같이 value 인자 부분의 타입이 unknown으로 나옵니다.

 

왜 그런 걸까요? 바로 new PersistentMemoryDB를 선언할 때 제네릭의 실제 테스트 타입이 빠졌기 때문입니다.

 

어떻게 해야 할까요? 만약 number로 value 타입을 하고 싶다면 다음과 같이 해야 합니다.

const myDB = new PersistentMemoryDB<number>();

즉, PersistentMemoryDB 클래스의 제네릭 T를 실제 사용할 때는 number로 지정하겠다는 뜻입니다.

 

자, 이제 위와 같이 value 인자의 타입이 number가 되었네요.

 

const myDB = new PersistentMemoryDB<number>();
myDB.set("foo",24);
console.log(myDB.get("foo"));
console.log(myDB.saveToString());
const saved = myDB.saveToString();

const myDB2 = new PersistentMemoryDB<number>();
myDB.set("foo", 25);
myDB2.restoreFromString(saved);
console.log(myDB.get("foo"));
console.log(myDB2.get("foo"));

실행 결과는 예측한 데로 나왔습니다.

 

두 번째, 클래스 제네릭에서 Key 부분을 추가해 볼까요?

 

Key 부분은 아래의 InMemoryDatabase 부분의 클래스 변수인 db의 Record 값 한 개인 string을 K로 바꾸는 겁니다.

class InMemoryDatabase<T> implements Database<T> {
    protected db: Record<string, T> = {};
    get(id: string): T {
        return this.db[id];
    }
    set(id: string, value: T): void {
        this.db[id] = value;
    }
}

 

Record 부분의 string을 K로 바꾸니까 K 이름을 찾을 수 없다고 에러가 뜨네요.

 

지금은 당연한 결과입니다.

 

Database interface 부분도 바꿔야 하기 때문입니다.

 

interface Database에도 K를 추가했고, InMemoryDatabase 클래스에도 K를 추가했습니다.

 

그런데 db 부분에 에러가 뜨는데요.

 

K 형식이 'string | number | symbol' 제약 조건을 만족하지 않는다고 나옵니다.

 

우리가 만든 interface와 클래스에 따르면 K 형식은 string, number, symbol 세 가지 중에 하나여만 하기 때문이죠.

 

그럼 위와 같이 에러코드는 어떻게 처리해야 할까요?

 

바로 Key 타입을 지정해줘서 K가 우리가 만든 Key 타입을 구현한다고 하면 됩니다.

 

type DBKeyType = string | number | symbol;
class InMemoryDatabase<T, K extends DBKeyType> implements Database<T, K> {
    protected db: Record<K, T> = {};
    get(id: K): T {
        return this.db[id];
    }
    set(id: K, value: T): void {
        this.db[id] = value;
    }
}

그래도 db 부분이 에러가 뜨는데요, 왜 그런 걸까요?

 

바로 db 부분의 초기화한 객체가 그냥 {}라고 되어 있는데, 빈 객체는 아무거나 될 수 있기 때문입니다.

 

그래서 초기화한 객체 {}를 올바른 타입으로 지정해줘야 합니다.

 

위와 같이 {} as Record<K, T>라고 명기했습니다.

 

이제 PersistentMemoryDB 부분도 바꿔야 합니다.

 

아래처럼 바꾸면 됩니다.

 

 

이제 테스트 코드에서 에러가 나오는데요.

 

바로 K 타입이 없기 때문입니다. 아래처럼요.

테스트 코드에 두 번째 부분은 DBKeyType 중에 하나를 넣으라고 하네요.

 

string을 넣어 보겠습니다.

 

const myDB = new PersistentMemoryDB<number, string>();
myDB.set("foo", 24);
console.log(myDB.get("foo"));
console.log(myDB.saveToString());
const saved = myDB.saveToString();

const myDB2 = new PersistentMemoryDB<number, string>();
myDB.set("foo", 25);
myDB2.restoreFromString(saved);
console.log(myDB.get("foo"));
console.log(myDB2.get("foo"));

실행 결과도 아까와 동일하게 잘 작동하고 있네요.

 

오늘은 클래스에서 제네릭을 쓰는 방법을 알아봤는데요.

 

솔직히 너무 헷갈리는데 오늘 강좌를 교과서 삼아서 공부하면 조금은 이해할 수 있을 겁니다.

 

 

그리드형