TypeScript 의 제네릭
이번엔, 제네릭에 대해서 살펴보도록 하자. 타입스크립트를 사용하게 되면서 가장 많이 마주칠지도 모른다.
제네릭(generic)
1
2
3
4
5
6
7
8
9
10
11
12
13
const person1 = {
type: 'human',
race: 'yellow',
name: 'zero',
age: 28,
}
const person2 = {
type: 'human',
race: 'yellow',
name: 'nero',
age: 32,
}
이 두 객체를 살펴보면, 이름(name
)과 나이(age
)를 제외하면, 다른 모든 변수들의 타입이 동일한 리터럴 타입이다. 이 둘의 타입을 인터페이스로 지정해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
interface Zero {
type: 'human',
race: 'yellow',
name: 'zero',
age: 28,
}
interface Nero {
type: 'human',
race: 'yellow',
name: 'nero',
age: 32,
}
Zero 와 Nero 두 인터페이스 간 이름과 나이를 제외한 변수들은 같은 리터럴 타입이니, 이 두 가지만큼이라도 덜 입력하게 만들면 좋을 것이다. 리터럴을 사용한다면 이것이 가능하니, 리터럴을 사용해서 타이핑을 해보자.
1
2
3
4
5
6
7
8
9
interface Person<N, A> {
type: 'human',
race: 'yellow',
name: N,
age: A,
}
interface Zero extends Person<'zero', 28> {}
interface Nero extends Person<'nero', 32> {}
제네릭을 통해, 어떤 타입으로 지정할 지 변수처럼 입력 받을 수 있다. Person 인터페이스를 상속받은 두 인터페이스 Zero 와 Nero 는 각각 name 과 age 에 ‘zero’, 28 과 ‘nero’, 32 로 타이핑이 되는 것이다.
제네릭은 인터페이스 뿐 아니라, 클래스, 함수 그리고 type 키워드를 이용한 타입 별칭에도 사용 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 클래스에 제네릭 사용
class Person<N, A> {
name: N;
age: A;
constructor(name: N, age: A) {
this.name = name;
this.age = age;
}
}
// 함수 선언문에서 제네릭 사용
function personFactoryD<N, A>(name: N, age: A) {
return ({
type: 'human',
race: 'yellow',
name,
age,
})
}
// 함수 표현식에서 제네릭 사용
const personFactoryE = <N, A>(name: N, age: A) {
return ({
type: 'human',
race: 'yellow',
name,
age,
})
}
// 타입 별칭에서 제네릭 사용
type Person<N, A> = {
type: 'human',
race: 'yellow',
name: N,
age: A,
}
type Zero = Person<'zero', 28>;
type Nero = Person<'nero', 32>;
이렇게 제네릭으로 설정하게 되면, 함수의 매개변수 처럼 기본값을 지정할 수 있다.
1
2
3
4
5
6
7
8
9
10
interface Person<N = string, A = number> {
type: 'human',
race: 'yellow',
name: N,
age: A,
}
type Person1 = Person; // Person<string, number> 로 기본값으로 타이핑
type Person2 = Person<number>; // Person <number, number>, 첫 번째 인수인 N 에만 타이핑이 됨. A 는 기본값.
type Person3 = Person<number, boolean>; // Person<number, boolean>
다만, 이전에 타입 추론을 적극적으로 사용하고, 타입 추론이 제대로 이루어지지 않아 직접 타이핑이 필요할 때만 사용하면 된다.
제네릭에 제약 걸기
앞서, 제네릭에 대해 다루면서 타입을 매개변수로 이용할 수 있다고 했다. 다만, 제네릭을 이용하더라도 타입에 제한을 걸어야할 때가 있다.
그럴 땐, extends 키워드를 이용하면 된다. 상속의 키워드와 같지만, 사용하는 방법은 다르다.
1
2
3
4
5
6
7
8
interface Example<A extends number, B = string> {
a: A,
b: B,
}
type Usecase1 = Example<string, boolean>; // Type 'string' does not satisfy the constraint 'number'.
type Usecase2 = Example<1, boolean>;
type Usecase3 = Example<number>;
현재 Example 인터페이스의 A 타입에 대해 제약을 걸어두었다. 타입 매개변수 A 는 number 타입이어야 한다는 의미로, Usecase1 처럼 사용할 경우엔 제약을 만족하지 못했다는 오류를 내개된다.
Usecase2, Usecase3 의 경우엔 보다 좁은 숫자 리터럴 1이나 number 타입으로 지정했기에, 제약을 만족하니 문제가 없는 것이다.
조금 더 깊게 다루면, 지금 필요한 지식 보다 내용이 깊어지기 때문에 이정도로 이해하고 오류 상황을 맞이하면 해결하면서 익혀보도록 해보자.