pnpm 혹은 yarn berry으로 모노레포 프로젝트를 사용하다보면 심심치 않게 등장하는 에러다.
원인은 간단하다,
의존성을 hoisting하여 평면적으로 설치하여 관리하는 npm이나 yarn과는 다르게, pnpm은 간접적인 의존성, 즉 X라는 의존성을 설치했을 때 X가 의존하는 Y들은 .pnpm
이라는 경로 아래에 따로 설치하고 심볼링크를 통해 접근한다. 이 때문에 간혹 Y에 있는 타입을 사용하려할 때 타입스크립트에서 경로를 찾아가지 못하는 문제가 발생하곤한다.
그리고 이 문제는 tsconfig
에서 "declaration": true
설정을 하는 경우에만 발생한다. 단순히 실행코드에서는 타입스크립트의 컴파일러가 함수의 매개변수나 리턴값 등을 찾아가면서 타입을 추론해주지만, 함수도 변수도 존재하지 않는 d.ts
파일에서는 기술적으로 타입스크립트 컴파일러가 타입을 추론해낼 수가 없다. 즉, 반드시 소스 코드내에서 명시적으로 타입을 찾을 수 있어야하는데, 이때 심볼링크가 연결된 간접의존성은 찾아가지 못하면서 발생하는 문제다.
관련해서 꽤 오래부터 pnpm
과 typescript
를 사용하는 경우 뜨거운 이슈였던 것으로 보이며 그 흔적이 여러군데 남아있다.
Typescript #29808
Typescript #42873
Typescript #47663
이 이슈와 관련해서 굉장히 다양한 카더라식 해결법들이 존재하는데, 이 코멘트 에서 해당 이슈의 원인과 해결방법들에 대해 꽤 상세하게 다루고 있으며, 그 해법들을 정리하여 소개한다.
앞서서 이 이슈가 "declaration": true
옵션에 의해 d.ts
파일을 만들려하는 과정에서 발생한다고 했다. 그렇다면 반대로 원인이 되는 이 옵션을 끄면 "declaration": false
문제가 발생하지 않는다.
다만, 이 경우 현재 프로젝트가 다른 프로젝트에 의해 import되지 않을 것이라는 전제가 필요하다.
아니면 타입스크립트의 유틸리티 타입을 이용해 타입을 명시적으로 표기하는 방법도 있다.
대표적인 express 예시로는,
import * as express from 'express';
const app: ReturnType<typeof express> = express();
처럼 서술적이지만 명시적으로 타입을 지정해주면 된다.
다만, 문제가 되는 함수나 메소드가 매개변수를 받거나 아니면 DI를 받아서 타입의 세부적으로는 유동적인 경우도 있는데, 이런 경우 제네릭을 지원한다면 역시 제네릭을 통해 명시적으로 타입을 표기하면 된다. 예를들어, ts-rest
라는 라이브러리가 있다.
import { nestControllerContract } from '@ts-rest/nest';
import { contract } from 'contract';
const c: ReturnType<typeof nestControllerContract<typeof contract>> = nestControllerContract(contract);
만약 제네릭을 지원하지 않거나 기타 다른 이유에서 명시적으로 타입을 표기할 수 없는 상황이라면 문제가 되는 패키지를 간접 의존성에 추가하면 된다.
me@Landing trpc % pnpm add -D --filter api @types/express-serve-static-core @types/qs @types/range-parser
Progress: resolved 941, reused 906, downloaded 0, added 0, done
Done in 5.6s
이러면 .pnpm
이 아닌 현재 워크스페이스의 node_modules
아래에 설치되기 때문에 참조하는데 문제가 없다. 단, 이 솔루션은 문제가 되는 패키지가 npm에서 설치한 경우에만 가능하며, 모노레포에서 공유하는 다른 워크스페이스라면 이 방법은 쓸 수가 없다.
그 외에 "preserveSymlinks": true
옵션을 사용하거나, .npmrc
등을 통해 pnpm이나 yarn berry에서도 의존성들을 hoisting하여 설치하는 방법, 혹은 alias path를 이용해 문제가 되는 의존성의 경로를 직접 잡아주는 방법들도 많이 볼 수 있는데, 이런 방법들은 그 결과 다른 문제들이 발생할 여지가 매우 많기에 아주 작은 사이드 프로젝트가 아닌 이상 권장하지 않는다.
좋은 글 감사합니다.