TypeScript의 제네릭 문법은 크게 함수, 클래스, 인터페이스에서 사용이 가능합니다.
제네릭 문법의 사용법을 아래의 예시 코드와 함께 살펴보도록 하겠습니다.
함수의 제네릭
interface Obj {
x: number;
}
type Arr = [number, number];
function toArray(a: string, b: string): string[]; // 함수의 타입 선언 1
function toArray(a: number, b: number): number[]; // 함수의 타입 선언 2
function toArray(a: boolean, b: boolean): boolean[]; // 함수의 타입 선언 3
function toArray(a: Obj, b: Obj): Obj[]; // 함수의 타입 선언 4
function toArray(a: Arr, b: Arr): Arr[]; // 함수의 타입 선언 5
function toArray(a: any, b: any) { // 함수의 구현
return [a, b];
}
console.log(
toArray("rati", "merry"),
toArray(1, 2),
toArray(true, false),
toArray({ x: 1 }, { x: 2 }),
toArray([1, 2], [3, 4])
);
인터페이스 Obj는 내부에 속성에 대한 타입이 명시되어 있으며, 타입 별칭 Arr의 경우 tuple 타입이 할당되어 있습니다. 그리고 함수 toArray는 오버로딩이 되어 각각의 매개 변수의 타입이 위와 같이 명시되어 있으며, 반환하는 값의 타입 또한 명시되어 있습니다.
함수 toArray의 각각의 매개 변수는 서로 동일한 타입의 인수를 전달받으며, 반환하는 배열의 요소의 타입도 동일합니다.
이렇게 함수를 사용할 때마다 타입에 따른 함수를 오버로딩하여 코드를 추가하기는 어려움이 있습니다. 이때 제네릭 문법을 사용하면 해당 문제를 개선할 수 있게 됩니다.
function toArray<T>(a: T, b: T) {
return [a, b];
}
기존의 함수의 타입 선언부를 모두 지워주고 함수의 구현부만 남긴 상태에서 꺾새(<>)안에 T라는 타입 변수를 만들어 주고, 만든 타입 변수 T를 매개 변수에 할당해 줍니다.
*꺾새 안의 타입 변수의 이름은 자유롭게 사용 가능하지만 일반적으로 타입의 정보를 의미하는 T를 사용합니다.
console.log(
toArray("rati", 1), // 타입 에러 발생
);
제네릭 문법을 적용한 함수를 사용하여 해당 함수에 인수로 서로 다른 타입의 데이터를 전달하게 되면 타입 에러가 발생하며, 오버 로딩한 함수와 동일한 결과가 나타나게 됩니다.
제네릭 문법이 적용되는 과정을 정리 해보면 꺾새 안에 만든 타입 변수 T는 인수를 전달받기 전까지 타입을 알 수 없는 상황입니다.
함수의 매개 변수 a에 인수로 rati라는 문자를 전달받은 시점에서 T는 string 타입이 되었으며, 함수의 매개 변수 b 또한 T(string 타입)를 전달받아야 하는데 인수로 number 타입이 들어와 위와 같은 타입 에러가 발생한 것입니다.
*위의 방식은 타입 변수에 들어온 데이터의 타입을 확인하고 타입스크립트가 T의 타입을 추론하는 방식입니다.
console.log(
toArray<string>("rati", 1),
);
또는 위와 같이 함수 호출부에서 꺾새 안에 타입을 명시해주게 되면 T는 string을 전달받게 되고, T를 전달받은 매개 변수 a와 b 또한 타입이 string으로 명시되게 됩니다.
*T의 타입을 함수 호출부에서 명시적으로 나타내주어도 좋지만 최대한 타입 추론을 이용해주는 것도 좋은 방법입니다.
type Arr = [number, number];
console.log(
toArray([1, 2], [3, 4, 5])
);
기존에 타입 별칭 Arr을 사용하여 타입을 명시해주었지만 해당 코드의 경우 제네릭 문법을 사용하고 별도의 형식을 명시해주고 있지 않기 때문에 tuple 타입의 Arr이 아니더라도 정상 작동하게 됩니다.
처음에 의도한 대로 tuple 타입의 인수를 전달받기 위해서는 아래와 같이 T의 타입을 명시해주어야 합니다.
console.log(
toArray<Arr>([1, 2], [3, 4, 5]) // 타입 에러 발생
);
이 경우 T는 배열 타입을 전달받으며, 해당 배열의 타입은 첫 번째 인수로 전달되는 요소를 기준으로 tuple 타입을 추론하기 때문에 타입 에러가 정상적으로 발생하게 됩니다.
클래스의 제네릭
예제 1
class User {
constructor(public payload) { // 타입 에러 발생
}
getPayLoad() {
return this.payload;
}
}
클래스 User는 constructor 함수의 매개 변수 payload에 접근 제어자를 설정해주어 클래스 속성 payload에 매개 변수 payload를 할당하고 있으며, getPayLoad 메서드를 사용하여 속성 payload의 값을 반환하고 있습니다.
현재의 예시 코드경우 매개 변수의 타입을 명시해주지 않아 위와 같은 타입 에러가 발생합니다. 이때 제네릭 문법을 사용하여 해당 문제를 해결할 수 있습니다.
class User<P> {
constructor(public payload: P) {
this.payload = payload;
}
getPayLoad() {
return this.payload;
}
}
클래스에서 제네릭 문법을 사용하는 방법은 클래스 명 뒤에 꺾새(<>)를 사용하여 내부에 payload를 의미하는 P라는 타입 변수를 만들어 주고 만들어진 타입 변수 P를 매개 변수이자 속성인 payload의 타입으로 명시해 줍니다.
예제 2
class User<P> {
constructor(public payload: P) {
this.payload = payload;
}
getPayLoad() {
return this.payload;
}
}
interface UserAType {
name: string;
age: number;
isValid: boolean;
}
interface UserBType {
name: string;
age: number;
emails: string[];
}
const rati = new User({
name: "rati",
age: 5,
isValid: true,
emails: [], // ?!
});
const merry = new User({
name: "merry",
// ?!
emails: ["email@email.com"],
});
변수 rati는 예시 1에서 생성한 클래스를 생성자 함수를 사용하여 호출하여 인수로 위와 같은 객체 데이터를 전달하고 있습니다.
interface UserAType {
name: string;
age: number;
isValid: boolean;
}
const rati = new User<UserAType>({
name: "rati",
age: 5,
isValid: true,
emails: [], // 타입 오류 발생
});
이때 변수 rati에 타입 변수 P에 인터페이스 UserAType을 할당한 클래스 인스턴스를 할당하고 싶어 위와 같이 코드가 수정되었습니다.
이렇게 제네릭 문법을 사용하여 인터페이스 UserAType로 타입을 명시해주게 되면 앞서 생성한 User 클래스의 타입 변수 P로 인터페이스 UserAType이 들어가게 되고, 타입 변수 P를 사용하여 타입이 명시된 payload의 타입 또한 인터페이스 UserAType으로 명시되게 됩니다.
이렇게 인터페이스 UserAType를 타입 변수에 할당한 클래스 인스턴스애 위와 같은 객체를 인수로 전달하게 되면 아래와 같은 타입 에러가 발생하게 됩니다.
인터페이스 UserAType에는 emails 속성에 대한 내용이 없으므로 위와 같은 타입 에러 메시지가 나타나게 됩니다.
*eamils 속성을 제거하면 타입 에러가 사라집니다.
interface UserBType {
name: string;
age: number;
emails: string[];
}
const merry = new User<UserBType> ({
name: "merry", // 타입 에러 발생
// ?!
emails: ["email@email.com"], // 타입 에러 발생
});
변수 merry의 경우 클래스 User의 타입 변수 P에 인터페이스 UserBType을 할당한 클래스 인스턴스를 할당받고 있습니다.
인터페이스 UserBType age 속성이 필수 속성인데 해당 클래스 인스턴스에는 age 속성이 없기 때문에 위의 타입 오류 메시지가 나타나게 됩니다.
*age 속성을 추가하면 타입 에러가 사라집니다.
예시 2처럼 동일한 클래스를 사용하여 인스턴스를 생성하지만 상황에 따라 다른 타입의 속성을 만들고 싶은 경우 제네릭 문법을 사용하게 되면 상황에 따라 유연하게 사용 가능한 클래스를 사용할 수 있게 됩니다.
인터페이스의 제네릭
interface MyDate<T> {
name: string;
value: T;
}
const dataA: MyDate<string> = {
name: "Date A",
value: "Hello world",
};
const dataB: MyDate<number> = {
name: "Date A",
value: 1234,
};
const dataC: MyDate<boolean> = {
name: "Date A",
value: true,
};
const dataD: MyDate<number[]> = {
name: "Date A",
value: [1, 2, 3, 4],
};
인터페이스 MyDate는 제네릭 문법을 사용하여 인터페이스 이름 뒤에 꺾새를 사용하여 내부에 타입 변수 T를 선언해주고 있으며, 해당 타입 변수를 속성 value에 할당하여 타입을 명시해주고 있습니다.
const dataA: MyDate<string> = {
name: "Date A",
value: "Hello world",
};
변수 dataA의 경우 제네릭 문법을 사용한 인터페이스 MyDate를 사용하여 타입을 명시해주고 있으며, 꺾새 내부에 string을 전달하고 있습니다.
이 경우 string은 타입 변수 T에 전달되고, 타입 변수 T를 사용하여 타입을 명시해주고 있는 속성 value의 타입은 string이 되게 됩니다.
때문에 속성 value의 값으로 문자 데이터를 할당할 수 있게 됩니다.
*dataB, dataC, dataD도 동일한 방법으로 인터페이스 MyDate의 타입 변수 T에 타입을 전달하고 있습니다.
제약 조건
위의 예시처럼 인터페이스에 제네릭 문법을 사용하게 되면 타입 변수에 어떠한 타입이 들어와도 정상적으로 작동하게 되는데 상황에 따라 필요한 타입을 추려 제한하고 싶을 수 있습니다. 이때 사용할 수 있는 것이 제네릭의 제약 조건입니다.
- extends를 사용한 제약 조건
interface MyDate<T extends string | number> {
name: string;
value: T;
}
타입 변수 T 뒤에 extends 키워드를 사용하여 union 타입을 명시해주게 되면 타입 변수 T에는 string 또는 number 타입만 들어올 수 있게 됩니다.
즉, 타입 변수는 extends 키워드 뒤에 오는 타입을 상속받게 됩니다.
- keyof를 사용한 제약 조건
function objectInKey<T extends object, U extends keyof T>(obj: T, key: U) {
return obj[key];
}
objectInKey({ name: "rati" }, age); // 타입 에러 발생
함수 objectInKey는 인수로 객체와 key 값을 전달받아 인수로 들어온 객체의 key 값을 반환하는 함수입니다.
해당 함수의 타입을 제네릭 문법을 사용하여 명시해 주었으며, 첫 번째 매개 변수에 할당된 T의 경우 object, 두 번째 매개 변수의 할당된 U의 경우 extends로 제약 조건을 추가함과 동시에 keyof 키워드를 사용하여 타입 변수 T로 제약하고 있습니다.
이 경우 타입 변수 T는 object로 타입이 명시되어 있기 때문에 타입 변수 U는 T로 제약 조건이 걸림과 동시에 keyof를 사용하여 타입 변수 T의 속성에 해당해야 됩니다. 첫 번째 인수로 전달하는 객체의 속성에는 age가 존재하지 않기 때문에 타입 에러가 발생하게 됩니다.
keyof 제약 조건을 사용하면 keyof 키워드 뒤에 오는 타입 변수에 존재하는 속성을 인수로 받을 수 받을 수 있게 됩니다.
'TypeScript' 카테고리의 다른 글
타입 가져오기 / 내보내기 (0) | 2023.01.03 |
---|---|
패키지의 타입 선언 (0) | 2023.01.03 |
클래스 (0) | 2023.01.02 |
함수 (0) | 2023.01.02 |
타입 별칭(Alias) (0) | 2023.01.02 |