핵심 표면
// index.d.ts
declare module "legacy-lib" {
export function parse(input: string): unknown;
}타입 공급
선언 파일을 다시 볼 때 먼저 구분하면 좋은 기본형은 아래입니다.
- 패키지 내장 타입: 패키지가
.d.ts를 함께 배포 - 외부 타입 패키지:
@types/... - 로컬 브리지 선언:
types/legacy-lib.d.ts - 전역 선언 보강:
declare global { ... }
1) 선언 파일은 구현 없이 타입 정보만 제공한다
.d.ts는 실제 런타임 로직이 아니라, 이 라이브러리가 어떤 값과 함수를 노출하는지 타입 수준에서 설명하는 파일입니다.
declare function sum(a: number, b: number): number;즉 선언 파일은 타입 정보 공급자이지 실행 로직의 일부가 아닙니다.
2) 외부 라이브러리 타입은 보통 두 경로로 온다
패키지 자체가 타입을 함께 배포할 수도 있고, 그렇지 않으면 @types/... 형태로 DefinitelyTyped에서 제공되는 경우가 많습니다.
npm install lodash
npm install -D @types/lodash라이브러리 타입 문제가 생기면 먼저 "패키지 내장 타입인가, 별도 @types인가"를 구분하는 편이 빠릅니다.
{
"name": "my-lib",
"types": "./dist/index.d.ts"
}3) 타입이 없는 라이브러리는 얇은 선언 파일로 브리지할 수 있다
레거시 JS 라이브러리를 잠시 써야 할 때는 필요한 API만 최소한으로 선언해서 TypeScript 프로젝트와 연결할 수 있습니다.
declare module "legacy-lib" {
export function parse(input: string): unknown;
}이때는 라이브러리 전체를 다 설명하려 하기보다, 지금 실제로 쓰는 API만 최소한으로 적는 편이 유지가 쉽습니다.
4) any로 대충 덮는 것보다 unknown 기반 최소 계약이 낫다
당장 편하려고 선언 파일 전체를 any로 덮으면, 그 경계 전체가 다시 느슨해집니다.
처음엔 얇더라도 실제 사용하는 함수 계약만큼은 조금 더 정확히 적는 편이 좋습니다.
5) 선언 파일은 생태계 경계를 읽는 카드이기도 하다
TypeScript 프로젝트에서는 코드만이 아니라 외부 패키지 타입 품질도 함께 따라옵니다.
그래서 .d.ts를 읽는 감각은 생태계 전체와 연결됩니다.
- 패키지 업데이트 후 타입만 깨진다: 내장 타입 변경 가능성
- 런타임은 되는데 타입만 없다:
@types또는 로컬 선언 필요 - 선언은 있는데 값이 다르다: 선언 파일과 실제 구현 불일치 가능성
어디서 타입이 오나
- 구현 없이 타입만 제공한다:
.d.ts - 패키지 자체가 타입을 포함한다: 내장 타입
- 패키지에 타입이 없다:
@types/...확인 - 레거시 라이브러리를 임시로 잇는다:
declare module - 선언을 대충 쓰고 싶어진다:
any대신 최소 계약 검토
주의할 점
선언 파일을 any로 대충 덮어두면 당장은 편해도, 실제 라이브러리 경계 전체가 느슨해집니다.
// ❌ 타입 정보가 거의 사라짐
declare module "legacy-lib" {
const value: any;
export default value;
}
// ✅ 최소한의 함수 계약은 남겨 둠
declare module "legacy-lib" {
export function parse(input: string): unknown;
}참고 링크
2 sources