⟦TypeScript〛‛A‛ is assignable to the constraint of type ‛T‛, but ‛T‛ could be instantiated with a different subtype of constraint ‛A‛.

2024-11-24에 씀

타입스크립트의 제네릭을 사용하다 보면 아래와 같은 에러를 마주치게 될 때가 있다.

'A' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'A'.

A가 T에 할당 가능하긴 한데 T가 A와 다른 서브타입으로 인스턴스화 될 수 있다고?

문제 상황 1.

Animal을 확장하는 어떤 타입 T가 있을 때, 이 타입에 해당하는 객체 배열을 인자로 받아서 무작위로 하나를 골라주는 getRandomAnimal 함수를 작성했다. animals 인자가 넘어오지 않은 경우에도 Animal 객체를 반환해 줄 수 있도록, animals 파라미터의 기본값으로 defaultAnimal 객체가 들어 있는 배열을 설정해 두었다.

1type Animal = { name: string };
2
3const defaultAnimal: Animal = { name: 'Bori' };
4
5const getRandomAnimal = <T extends Animal>(
6 animals: T[] = [defaultAnimal]
7): T => {
8 const randomIndex = Math.floor(Math.random() * animals.length);
9 return animals[randomIndex];
10}

그런데 animals의 기본값을 설정하면 아래와 같은 에러가 발생한다.

1Type 'Animal' is not assignable to type 'T'.
2 'Animal' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Animal'.

문제 상황 2.

이번에는 파라미터 기본값을 설정하지 않았다. 대신, animals가 빈 배열이라면 Animal 타입의 defaultAnimal을 반환하게 만들었다.

1const defaultAnimal: Animal = { name: 'Bori' };
2
3const getRandomAnimal = <T extends Animal>(
4 animals: T[] = []
5 ): T => {
6 if (animals.length === 0) {
7 return defaultAnimal; // Error! Type 'Animal' is not assignable to type 'T'.
8 }
9 const randomIndex = Math.floor(Math.random() * animals.length);
10 return animals[randomIndex];
11}

이번에는 return defaultAnimal 부분에서 똑같은 에러가 발생한다.

다시, 발생한 에러를 살펴보자.

1Type 'Animal' is not assignable to type 'T'.
2 'Animal' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Animal'.

에러를 읽어보면 아래와 같은 내용이다.

TAnimal을 확장하는데 AnimalT에 할당 가능하지 않다니?

타입스크립트가 막고자 하는 것

getRandomAnimal은 제네릭 함수여서, 함수를 호출하는 시점에 제네릭 T의 타입을 명시해 줄 수 있다.

1const animal = getRandomAnimal<Animal>();

이 경우, 인자를 넘기지 않았기 때문에 getRandomAnimal은 기본값에 설정된 defaultAnimal 을 반환할 것이다. defaultAnimal의 타입은 Animal 타입이고, animal 변수의 타입도 Animal이므로 문제 없이 동작한다.

이번에는 Animal의 하위 타입인 Bird 타입이 있다고 해 보자. 그리고 Bird 타입에 대해서 getRandomAnimal 함수를 호출하는 상황을 생각해 보자.

1type Bird = Animal & { fly: () => void };
2
3const bird = getRandomAnimal<Bird>();
4 // ^? const bird: Bird
5
6bird.fly(); // Error - Uncaught TypeError: bird.fly is not a function

Bird 타입의 객체는 fly 메서드를 가진다. 그리고 getRandomAnimal을 호출할 때 제네릭으로 Bird 타입을 지정했기 때문에, getRandomAnimal<Bird> 의 반환 타입은 Bird가 되어야 한다.

인자를 넘기지 않았기 때문에 getRandomAnimaldefaultAnimal 객체를 반환하며, 이 객체는 fly 메서드가 없다. 따라서 bird 타입인 줄 알고 fly 메서드를 호출하려고 하면 에러가 발생하게 된다.

아까 발생한 에러를 다시 생각해 보자.

따라서, Animal은 T에 할당 가능하지 않다.

해결 방법

이런 문제를 해결하려면, 기본값을 설정하지 않거나, 위험을 무릅쓰고 defaultAnimal의 타입을 T로 단언해 줄 수 있다.

1// 1. 기본값을 주지 않는다
2const getRandomAnimal = <T extends Animal>(
3 animals: T[]
4): T => {
5 const randomIndex = Math.floor(Math.random() * animals.length);
6 return animals[randomIndex];
7}
8
9// 2-1. 위험을 무릅쓰고 defaultAnimal의 타입을 T로 단언해준다
10const getRandomAnimal = <T extends Animal>(
11 animals: T[] = [defaultAnimal as T]
12): T => {
13 const randomIndex = Math.floor(Math.random() * animals.length);
14 return animals[randomIndex];
15}
16
17// 2-2. 위험을 무릅쓰고 defaultAnimal의 타입을 T로 단언해준다
18const getRandomAnimal = <T extends Animal>(
19 animals: T[] = []
20 ): T => {
21 if (animals.length === 0) {
22 return defaultAnimal as T;
23 }
24 const randomIndex = Math.floor(Math.random() * animals.length);
25 return animals[randomIndex];
26}

혹은, defaultAnimal을 T 타입으로 변환해주는 함수를 인자로 받게 할 수도 있다. 가장 확실한 방법이긴 하지만, 아래 코드처럼 convertAnimal과 같은 함수를 따로 구현해야 한다는 단점이 있다.

이 경우, defaultAnimal이 필요하지 않은 경우에도 convertAnimal을 구현해야 할 수도 있다는 단점이 있다. 또, Bird 타입의 경우에는 fly 메서드가 추가로 필요한데, defaultAnimal에서는 어떻게 구현해 주어야 할 것인가를 고민해야 한다.

1const getRandomAnimal = <T extends Animal>(
2 animals: T[] = [],
3 // 외부에서 Animal을 T 타입으로 변환하는 함수를 넘겨준다
4 convertAnimal: (animal: Animal) => T
5 ): T => {
6 const randomIndex = Math.floor(Math.random() * animals.length);
7 if (randomIndex > -1) {
8 return animals[randomIndex];
9 }
10 // defaultAnimal을 T 타입으로 변환해서 안전하게 사용한다
11 return convertAnimal(defaultAnimal);
12}
13
14getRandomAnimal<Bird>(
15 [],
16 (animal: Animal) => ({ ...animal, fly: () => {} })
17);

결론적으로, 제네릭 타입이 사용되는 함수에서는 제네릭 타입이 올 수 있는 곳에 제네릭의 하위 타입이 될 수 없는 값을 사용하지 않는 것이 가장 안전하다.

프로필 사진

조예진

이전 포스트
CSS Modules와 CVA로 스타일 변형 관리하기