제너릭 - Class (1)

Changhan·2025년 2월 12일

Typescript

목록 보기
28/29

클래스에서의 제너릭

class Singer<S, N> {
  name: S;
  age: N;

  constructor(name: S, age: N) {
    this.name = name;
    this.age = age;
  }
}
const taeyeon = new Singer<string, number>('태연', 32);
console.log(taeyeon);

class에서 제너릭은 클래스명 옆에 제너릭 변수를 넣어주면 된다. 그리고 인스턴스를 생성할 때 제너릭 타입을 넣어주면 된다.

class Pagination2<Data, Message> {
  data: Data[] = [];
  message?: Message;
  lastFetchedAt?: Date;

  constructor(data: Data[], message?: Message, lastFetchedAp?: Date) {
    this.data = data;
    this.message = message;
    this.lastFetchedAt = lastFetchedAp;
  }
}
const pagination = new Pagination2([1234, 123]);
pagination.data; // type: number
pagination.message; // type: unknown
pagination.lastFetchedAt; // type: Date | undefined

pagination.data는 number 타입이 된다. 인스턴스를 만들 때 [1234, 123] 배열을 넣어줘서 이 배열을 가지고 타입 추론을 한 것이기 때문이다.

pagination.message는 unknown 타입이 된다. message는 optional 값이긴 하지만 제너릭 타입을 넣어주지도 않았고, 생성자 함수에서 Message에 어떤 값을 넣어주지도 않았기 때문에 어떤 타입인지 알 수 없는 것이다.

pagination.lastFetchedAt은 클래스 내부에서 Date라는 타입을 선언했다. 그리고 이 프로퍼티는 optional이기 때문에 타입이 Date 또는 undfined인 것이다.

기본 타입 설정

class DefaultGeneric<T = boolean> {
  data: T[] = [];
}
const defaultgeneric = new DefaultGeneric();
defaultgeneric.data; // type: boolean[]

클래스 또한 제너릭 타입을 기본값으로 설정할 수 있다.


상속에서의 제너릭

class Song<T> {
  songName: T;
  constructor(songName: T) {
    this.songName = songName;
  }
}
class Singer extends Song<string> {}
const singer = new Singer('');
singer.songName; // string

상속받은 클래스는 부모 클래스의 제너릭을 그대로 사용할 수 있다.

class BaseCache<T> {
  data: T[] = [];
}
class GenericChild<T, Message> extends BaseCache<T> {
  message?: Message;
  constructor(message?: Message) {
    super();
    this.message = message;
  }
}
const genericChild = new GenericChild<string, string>('error');
genericChild.data;
genericChild.message; // string | undefined인 이유는 optional이기 때문

자식 클래스에서 선언한 제너릭 타입을 그대로 부모 클래스에도 넘겨줄 수 있다. 자식 클래스에서 제너릭을 <string, string>으로 선언했으므로 부모 클래스인 BaseCache의 제너릭 타입은 string을 받는다. 따라서 genericChild.data의 타입은 string[]이다.

제너릭의 상속

제너릭이 선언된 클래스의 상속이 아닌 제너릭 자체를 상속할 수도 있다.

interface BaseGeneric {
  name: string;
}
class Idol<T extends BaseGeneric> {
  information: T;
  constructor(information: T) {
    this.information = information;
  }
}
const yujin = new Idol({ name: '안유진', age: 23 });
const yujin2 = new Idol({ name: '안유진' });
const yujin3 = new Idol({ age: 23 }); // error, 

Idol 클래스에 객체를 입력받는다. 입력받은 객체는 information 프로퍼티에 들어간다. 그럼 T 타입은 우리가 입력받은 객체가 된다.
여기서 T 타입은 BaseGeneric 인터페이스를 확장한 것이기 때문에 T 타입에는 BaseGeneric의 프로퍼티를 모두 가지고 있는 형태여야 한다. 따라서 우리가 입력받은 객체에 name 프로퍼티는 필수적인 것이다.

keyof 키워드를 이용한 제너릭

다음은 실수할 수 있는 상황의 예시다.

const testObj = {
  a: 1,
  b: 2,
  c: 3,
};

function objParser<O>(obj:O, key: string) {
  return obj[key];
}

objParser는 객체와 키 값을 이용해 해당 객체의 값을 받아오는 함수다. 그래서 객체의 제너릭으로 O를 받고, key는 타입으로 string으로 받았다.
하지만 이렇게 하면 에러가 발생한다. 왜냐하면 현재 obj는 어떤 타입인지 알 수가 없는데(unknown) 이 값에 string을 넣으려고 해서 발생하는 에러다.

어떻게?

function objParser<O, K extends keyof O>(obj: O, key: K) {
  return obj[key];
}
const value = objParser(testObj, 'a');
const value2 = objParser(testObj, 'g'); // error

K라는 파라미터는 나중에 입력할 객체의 키 값을 무조건 넣을 것이라는 걸 명시해줘야 한다.

K는 O 타입의 키 값들의 유니온을 extend한다.
위에서 살펴봤듯이 extends를 사용하면 해당 타입의 프로퍼티를 모두 가지고 있어야 했다. 따라서 K는 O의 키 값들을 모두 가지고 있어야 한다는 것이다.

함수를 호출할 때 testObj의 키 값이 아닌 다른 것은 넣을 수 없게 된다.

0개의 댓글