- Published on
TS 5.5+ isolatedDeclarations 오류 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서로 다른 팀이 만든 TS 라이브러리를 합치거나, declaration을 켠 채로 빌드 파이프라인을 강화하는 순간 TS 5.5+에서 갑자기 isolatedDeclarations 관련 오류가 터지는 경우가 많습니다. 특히 tsup·rollup·vite로 번들링하면서 타입 선언 파일(.d.ts)을 생성하는 프로젝트라면, 기존에는 통과하던 코드가 “선언 파일을 안전하게 생성할 수 없음”이라는 이유로 막히곤 합니다.
이 글은 TS 5.5+의 isolatedDeclarations가 무엇을 강제하는지, 어떤 코드가 왜 깨지는지, 그리고 실무에서 가장 빠르게 고치는 패턴을 모아둔 해결 가이드입니다.
관련 글도 함께 참고하면 좋습니다: TS 5.5+ isolatedDeclarations 에러 실전 해결법
isolatedDeclarations가 하는 일: “파일 단위로 d.ts를 만들 수 있어야 함”
isolatedDeclarations는 말 그대로 각 소스 파일을 다른 파일의 구현에 의존하지 않고 타입 선언으로 변환 가능해야 한다는 제약을 겁니다. 즉, 어떤 파일의 .d.ts를 만들 때 컴파일러가 “전체 프로그램을 다 분석해서 추론”하기보다, 파일 자체만 보고도 안전한 선언을 만들 수 있어야 합니다.
이 옵션이 강화된 배경은 다음과 같습니다.
- 빌드 성능 및 증분 컴파일 최적화
- 번들러/트랜스파일러 생태계에서 “파일 단위 변환”이 일반적
- 라이브러리 배포 시
.d.ts품질을 안정화
따라서 TS 5.5+에서 이 옵션을 켜면(혹은 도구/템플릿이 기본으로 켜면) 아래 같은 코드들이 자주 걸립니다.
빠른 진단 체크리스트
먼저 tsconfig.json에서 아래 조합을 확인하세요.
compilerOptions.declaration:truecompilerOptions.emitDeclarationOnly:true또는 번들러가 d.ts만 따로 생성compilerOptions.isolatedDeclarations:true
그리고 에러는 대체로 다음 범주로 나뉩니다.
- export되는 값의 타입이 추론에 의존해서
.d.ts로 안전하게 못 나감 - 로컬(비-export) 타입에 의존하는 export
- 조건부/동적 패턴(예: 런타임 분기, 복잡한 팩토리)으로 타입을 “암시”만 함
- default export + 익명 타입/함수 조합
아래에서 원인별로 가장 흔한 패턴과 해결책을 코드로 정리합니다.
패턴 1: export const에 타입 주석이 없어서 깨짐
가장 흔합니다. 구현에서 타입이 추론되지만, 그 타입이 외부로 노출될 때 선언 생성이 불안정하면 에러가 납니다.
문제 코드
// api.ts
export const client = {
get(path: string) {
return fetch(path).then(r => r.json());
},
};
이 코드는 client.get의 반환 타입이 Promise<any> 비슷하게 추론되거나, DOM lib/환경에 따라 미묘하게 달라질 수 있습니다. isolatedDeclarations는 이런 “환경 의존 추론”을 싫어합니다.
해결: export되는 심볼에 명시적 타입 부여
// api.ts
export type HttpClient = {
get: (path: string) => Promise<unknown>;
};
export const client: HttpClient = {
get(path) {
return fetch(path).then(r => r.json());
},
};
핵심은 “export되는 값(변수/함수/클래스)의 타입을 .d.ts로 안정적으로 표현”하는 것입니다.
패턴 2: export가 로컬 타입(비-export)에 의존
.d.ts는 외부 소비자가 보는 계약인데, 그 계약이 파일 내부에만 존재하는 타입을 참조하면 선언 생성이 꼬일 수 있습니다.
문제 코드
// user.ts
type UserId = string;
export function loadUser(id: UserId) {
return { id };
}
UserId가 export되지 않았는데 loadUser 시그니처에 등장합니다.
해결 1: 필요한 타입을 export
// user.ts
export type UserId = string;
export function loadUser(id: UserId) {
return { id };
}
해결 2: 외부로 노출되는 시그니처에서 로컬 타입 제거
// user.ts
type UserId = string;
export function loadUser(id: string) {
const _id: UserId = id;
return { id: _id };
}
라이브러리 API 계약에 포함되어야 하는 타입이면 export하는 편이 보통 더 낫습니다.
패턴 3: 반환 타입이 구현에 과도하게 의존(복잡한 객체 리터럴)
객체 리터럴을 그대로 export하면, TS가 “정확한 리터럴 타입”을 만들려고 하다가 선언 생성 단계에서 제약에 걸릴 수 있습니다.
문제 코드
// config.ts
export const config = {
mode: process.env.NODE_ENV === "production" ? "prod" : "dev",
retry: 3,
};
process.env는 환경/타입 정의에 따라 흔들리고, mode는 리터럴 유니온으로 예쁘게 추론되기도 하지만 선언 생성 관점에서는 불안정해질 수 있습니다.
해결: 타입을 고정하고 구현은 그 타입을 만족시키게
// config.ts
export type AppConfig = {
mode: "prod" | "dev";
retry: number;
};
export const config: AppConfig = {
mode: process.env.NODE_ENV === "production" ? "prod" : "dev",
retry: 3,
};
또는 값 자체를 고정하고 싶으면 as const를 쓰되, 외부로 노출되는 타입은 별도로 export하는 전략이 안전합니다.
// config.ts
const _config = {
mode: "dev",
retry: 3,
} as const;
export type AppConfig = {
mode: "prod" | "dev";
retry: number;
};
export const config: AppConfig = _config;
패턴 4: default export 익명 함수/클래스
export default () => {} 같이 익명으로 내보내면 선언 생성 시 이름 부여/참조가 애매해져 오류를 유발하는 경우가 있습니다.
문제 코드
// index.ts
export default function () {
return { ok: true };
}
해결: 이름을 붙이고 타입을 명시
// index.ts
export type HandlerResult = { ok: boolean };
export default function handler(): HandlerResult {
return { ok: true };
}
혹은 default export를 피하고 named export로 통일하는 것도 팀 규칙으로 효과가 큽니다.
패턴 5: 오버로드/조건부 타입이 구현과 섞여 선언이 불안정
오버로드 자체는 괜찮지만, 구현부에서 반환 타입을 추론에 맡기면 종종 걸립니다.
문제 코드
// parse.ts
export function parse(input: string) {
if (input.startsWith("{")) return JSON.parse(input);
return input;
}
해결: 오버로드로 외부 계약을 고정
// parse.ts
export function parse(input: string): unknown;
export function parse(input: string): unknown {
if (input.startsWith("{")) return JSON.parse(input);
return input;
}
더 구체적으로 하고 싶다면 제네릭을 쓰되, 제네릭 표기는 반드시 인라인 코드로 다루거나 문서에서 엔티티 처리해야 합니다. 코드 블록에서는 안전합니다.
// parse.ts
export function parseJson<T>(input: string): T {
return JSON.parse(input) as T;
}
패턴 6: 타입이 외부 모듈의 “값”에서 유도됨
값에서 타입을 뽑아 쓰는 패턴은 강력하지만, export 경계에서 선언 생성이 복잡해지면 문제가 됩니다.
문제 코드
// routes.ts
const routes = {
home: "/",
about: "/about",
};
export type RouteKey = keyof typeof routes;
export const ROUTES = routes;
이 자체는 괜찮을 때도 많지만, routes가 다른 파일/조건부 로직/동적 병합을 포함하면 깨질 확률이 올라갑니다.
해결: export되는 값은 처음부터 export하고 형태를 고정
// routes.ts
export const ROUTES = {
home: "/",
about: "/about",
} as const;
export type RouteKey = keyof typeof ROUTES;
이렇게 하면 .d.ts가 훨씬 예측 가능해집니다.
tsconfig 운영 전략: 언제 켜고, 어디서 끄나
isolatedDeclarations는 “끄면 편해지는” 옵션이지만, 라이브러리 배포/모노레포 공용 패키지에서는 켜두는 편이 장기적으로 이득입니다. 대신 적용 범위를 나누는 전략이 좋습니다.
추천 1: 앱과 라이브러리 tsconfig 분리
- 앱(Next.js 등): 선언 파일 배포가 목적이 아니면
declaration자체를 끄는 경우가 많음 - 라이브러리(packages/*):
declaration: true+isolatedDeclarations: true로 계약 품질 보장
예시:
// packages/foo/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"isolatedDeclarations": true,
"outDir": "dist/types"
},
"include": ["src"]
}
추천 2: d.ts 생성 전용 빌드 스텝을 분리
번들링은 esbuild/swc로 하고, 타입은 tsc로만 뽑는 방식이 흔합니다.
// package.json
{
"scripts": {
"build": "npm run build:js && npm run build:types",
"build:js": "tsup src/index.ts --format esm,cjs",
"build:types": "tsc -p tsconfig.json"
}
}
이때 build:types에서 isolatedDeclarations가 켜져 있으면, 타입 배포 품질이 올라가고 CI에서 문제를 조기에 잡을 수 있습니다. CI 최적화 관점은 GitHub Actions 매트릭스로 CI 시간 50% 줄이기 같은 글과도 결이 같습니다.
실전 수정 흐름: “외부로 나가는 타입만” 고정하라
대부분의 팀에서 isolatedDeclarations 대응은 전 파일에 타입을 도배하는 방식으로 과잉 대응하기 쉽습니다. 하지만 비용 대비 효과가 좋은 지점은 명확합니다.
- export되는 함수/변수/클래스의 공개 시그니처에 타입 주석 추가
- 공개 API에서 참조하는 타입은 반드시 export
- 객체 리터럴 export는
type또는interface로 외형을 고정 - default export 익명 패턴 제거
이 4가지만 지켜도 오류의 대부분이 사라집니다.
자주 묻는 질문
Q1. 무조건 isolatedDeclarations를 켜야 하나요?
앱 코드만 있고 라이브러리 배포가 목적이 아니라면 필수는 아닙니다. 다만 모노레포에서 공용 패키지를 운영하거나, 외부에 배포하는 SDK/유틸 패키지가 있다면 켜두는 것이 장기적으로 안전합니다.
Q2. isolatedModules와는 뭐가 다른가요?
isolatedModules: 파일 단위로 JS 변환이 가능해야 함(주로 트랜스파일러 호환)isolatedDeclarations: 파일 단위로.d.ts생성이 가능해야 함(타입 선언 품질)
서로 목적이 다르지만, 함께 켜진 프로젝트가 많고 “추론 의존 코드를 줄이라”는 방향성은 비슷합니다.
마무리
TS 5.5+의 isolatedDeclarations 오류는 “타입이 틀렸다”기보다 “선언 파일로 내보내기엔 계약이 불명확하다”는 신호인 경우가 많습니다. 공개 API 경계에서 타입을 명시하고, 로컬 타입 의존을 정리하고, 객체 리터럴 export를 안정화하면 대부분 해결됩니다.
한 번 정리해두면 이후에는 CI에서 타입 배포 품질을 지속적으로 보장할 수 있고, 소비자 입장에서도 .d.ts가 예측 가능해져 DX가 좋아집니다.