← 전체 글로 돌아가기

TypeScript

TypeScript 제네릭 타입 에러가 빌드 타임에만 날 때

TypeScript에서 제네릭 사용 시 로컬에서는 문제가 없는데 빌드할 때만 타입 에러가 발생하는 경우를 해결하는 방법을 정리했다.

로컬 개발할 때는 타입 에러가 없는데, 빌드할 때 갑자기 제네릭 타입 에러가 나올 때가 있다. 이는 보통 TypeScript 컴파일러가 타입을 추론하지 못하거나, 타입 정의가 불완전할 때 발생한다.

제약 없는 제네릭 vs 제약이 있는 제네릭

제네릭을 정의할 때 <T>만 쓰면 T는 어떤 타입이든 될 수 있다. 하지만 특정 인터페이스를 구현해야 한다는 제약을 걸 수도 있다. <T extends Base>처럼 extends 키워드를 사용하면 T가 Base 타입이어야 한다는 조건을 붙일 수 있다. IDE에서는 이를 놓칠 수 있지만, 빌드 타임 컴파일러는 엄격하게 검사한다.

제네릭 타입 매개변수 확인하기

API 응답 타입이 Response<T>로 정의되어 있다면, 실제로 사용할 때 T를 명시적으로 지정했는지 확인하자. Response 하나만 쓰면 T가 unknown이 되어 나중에 문제가 생길 수 있다. 항상 Response<User> 같이 구체적인 타입을 명시하자.

npm run build
npx tsc --noEmit --strict

타입 변수의 분포성(Distributivity) 이해하기

제네릭 타입에 유니언이 포함되면 복잡해진다. <T extends A | B>일 때 T는 A이거나 B여야 하는데, 조건부 타입 안에서는 분포적으로 처리된다. 이 부분이 실제로 어떻게 동작하는지 문서를 읽거나 타입 테스트를 작성해서 확인해보자.

as const로 타입 추론 강제하기

객체를 함수에 전달할 때, TypeScript는 객체의 타입을 넓은 범위로 추론할 수 있다. as const 단언을 사용하면 더 구체적인 타입으로 추론하게 할 수 있다. 이는 제네릭 타입 추론을 정확하게 하는 데 도움이 된다.

재귀 제네릭의 깊이 제한

깊게 중첩된 객체 타입을 다루는 제네릭을 작성하면, TypeScript가 타입을 무한으로 확장하려고 할 수 있다. 이럴 때는 재귀 깊이를 제한하거나, never 타입을 사용해서 종료 조건을 명시적으로 표현해야 한다.

제네릭 함수의 오버로드 사용

한 함수가 여러 다른 입력 타입을 받아야 한다면, 제네릭만으로는 부족할 수 있다. 함수 오버로드를 사용해서 구체적인 케이스들을 따로 정의하면, 타입 추론이 더 정확해진다.

타입 테스트 파일 작성하기

복잡한 제네릭을 사용할 때는 타입이 정확한지 확인하는 테스트를 별도로 작성하는 게 좋다. type AssertEquals<A, B> = A extends B ? (B extends A ? true : false) : false 같은 유틸리티를 사용해서 예상 타입과 실제 타입이 일치하는지 검증할 수 있다.

엄격한 타입 체크 옵션 활용

tsconfig.json에서 strict: true로 설정하면 타입 체크가 매우 엄격해진다. 개발 중에는 체크를 조금 낮춰두고, 빌드 전에 --strict 플래그를 붙여서 최종 검사를 하는 방법도 있다. 이렇게 하면 미묘한 타입 문제를 조기에 발견할 수 있다.