[ 목차 ]
Introduction
제네릭 맛보기
제네릭 종류 - 함수, 인터페이스, 클래스
제네릭 제약 조건 (constraints)
제네릭 디폴트 파라미터
Introduction
타입스크립트에서 제네릭은 빠질 수 없는 개념이 되었습니다.
유틸함수도 제네릭을 사용하고 있고, API 인터페이스를 작성하거나 함수를 사용할 때도 많이 사용하고 있습니다.
제네릭에 대해서 타입을 위한 함수와 같다는 표현을 사용하는 것도 보았는데요.
제네릭은 타입에 대한 중복을 제거해줄 수 있기 때문에 이런 표현을 사용한 것 같습니다.
제네릭 맛보기
타입스크립트에서 제네릭은 함수, 인터페이스, 클래스 등에 사용할 수 있습니다.
가장 일반적인 방법은 함수에 제네릭을 사용하는 것일 겁니다.
function age(value: number): number {
return age;
}
function age(value: string): string {
return age;
}
편의상, 다른 타입의 같은 이름을 가진 age 함수를 두 개 작성했습니다.
value 매개변수 타입은 위에서 number 또는 string으로 표현이 되고 있는데요.
매개변수 타입을 제외하면 함수 형태가 동일하기 때문에, 중복된 함수라고 볼 수 있습니다.
매개변수 타입이 늘어난다면 비슷한 함수를 계속 만들어야 하는 문제가 발생합니다.
function age<T>(value: T>: T {
return value;
}
age<number>(25); // 25
age<string>('25살'); // 25살
age(30); // 30
age('30살'); // 30살
위와 같이 <>를 사용해서 제네릭을 적용할 수 있습니다.
함수를 호출할 때에 <>를 사용해서 타입을 지정해줄 수 있고, 그렇지 않아도 타입추론이 되는 경우도 있습니다.
function age<Type>(value: Type>: Type {
return value;
}
// 타입 파라미터를 여러 개 사용할 수도 있습니다.
function age<T1, T2>(value1: T1, value2: T2): T2 {
console.log(value1);
return value2;
}
<>에 들어가는 타입을 타입 파라미터(Type Parameter) 라고 부릅니다.
일반적으로 T를 많이 사용하지만, 네이밍은 마음대로 정할 수 있습니다.
제네릭 종류
1. 제네릭 함수
// 함수 선언문
function age<T>(value: T): T {
return value;
}
// 함수 표현식
const age2: <T>(value:T) => value = age;
// 객체 리터럴 타입의 함수 호출 시그니처
const age3: { <T>(value: T): T } = age2;
2. 제네릭 인터페이스
// 예시1
interface Age<T> {
value: T;
}
// 예시2
interface AgeFunc {
<T>(value: T) => T;
}
// 함수에 인터페이스를 적용
const age: AgeFunc = (value) => value;
age(11); // 11
age('11살') // '11살'
3. 제네릭 클래스
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const p = new Person('Rabbit', 10);
p.name; // 'Rabbit'
p.age; // 10
위와 간단한 클래스를 만들어봤는데요.
간단하니 한번 빠르게 읽어보세요.
제네릭 클래스로 바꿔보겠습니다.
// name에 대한 타입을 N, age에 대한 타입을 A로 표현했습니다.
class Person<N, A> {
name: N;
age: A;
constructor(name: N, age: A) {
this.name = name;
this.age = age;
}
}
const p = new Person('Rabbit', 10);
p.name; // 'Rabbit'
p.age; // 10
제네릭 제약 조건 (constraints)
제네릭 타입을 지정할 때, extends 키워드를 이용해서 제약 조건을 걸어줄 수 있습니다.
어떤 타입으로 특정할 수도 있고요.
어떤 형태를 가지고 있으면 해당 타입을 허용합니다. (덕 타이핑)
// 예시1
function age<T extends number>(value: T): T {
return value;
}
age('10살'); // error!
age(10); // ok
// 예시2
function getPerson<T extends { name: string }>(person: T): T {
return person
}
getPerson({ age: 10 }); // error!
getPerson({ name: 'Rabbit', age: 10}); // ok
// 예시3
function getKeyOfPerson<T, K extends keyof T>(person, key) {
return person[key];
}
const person = { name: 'Rabbit, age: 10 };
getKeyOfPerson(person, 'work'); // error!
getKeyOfPerson(person, name); // ok
제네릭 디폴트 타입 파라미터
<> 안의 타입 파라미터에 디폴트 타입을 지정해줄 수 있습니다.
ES6에서 함수 파라미터에 디폴트 값을 지정해줄 수 있는 것과 비슷한데요.
마땅한 예제를 찾기 쉽지 않아서, axios 타입 선언 일부를 가져와 봤습니다.
// 원본 인터페이스: Axios 응답을 표현하는 인터페이스
interface AxiosResponse<T = any, D = any> {
data: T;
status: number;
statusText: string;
headers: AxiosResponseHeaders;
config: AxiosRequestConfig<D>;
request?: any;
}
// 위의 인터페이스를 더 간단하게 수정하겠습니다.
interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
request?: any;
}
수정한 AxiosResponse 인터페이스를 보면, T 타입 파라미터에 any 타입이 디폴트로 지정된 것을 볼 수 있습니다.
AxiosResponse 인터페이스를 이용해서 객체를 선언할 때, 타입 추론을 못하거나 타입을 넘겨주지 않으면 any 타입이 T에 기본적으로 적용된다는 의미입니다.
interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
request?: any;
}
// 예시 1 - ok
const response: AxiosResponse = {
data: 1,
status: 200,
statusText: 'Good',
request: {}
}
// 예시 2 - error!
const response: AxiosResponse<string> = {
data: 1,
status: 200,
statusText: 'Good',
request: {}
}
예시1 에서는 T에 타입을 지정하지 않아서 any 타입이 data에 적용됩니다.
그래서 오류가 발생하지 않습니다.
하지만, 예시2 에서는 T에 string 타입을 지정했습니다.
data에 1을 넣어주었기 때문에, 타입이 일치하지 않아 오류가 발생합니다.
'TypeScript' 카테고리의 다른 글
[Typescript] Mapped Type (매핑된 타입) (0) | 2023.01.07 |
---|---|
[Typescript] 생성자의 타입을 표현하기 (feat. 생성자 시그니처) (0) | 2023.01.07 |
[Typescript] keyof 연산자 (feat. JS에서는 없어요) (0) | 2023.01.07 |
[Typescript] typeof 연산자 (with. JS에서의 typeof 연산자) (0) | 2023.01.07 |
[Typescript] 트리플 슬래시 지시어 (Triple-Slash Directives) (0) | 2022.10.02 |