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 프로퍼티는 필수적인 것이다.
다음은 실수할 수 있는 상황의 예시다.
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의 키 값이 아닌 다른 것은 넣을 수 없게 된다.