Post

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 타입으로 지정했기에, 제약을 만족하니 문제가 없는 것이다.

조금 더 깊게 다루면, 지금 필요한 지식 보다 내용이 깊어지기 때문에 이정도로 이해하고 오류 상황을 맞이하면 해결하면서 익혀보도록 해보자.

This post is licensed under CC BY 4.0 by the author.