선언 파일(.d.ts)은 타입만 담습니다. 다른 곳에 있는 JavaScript에 대한 시그니처, 인터페이스, 클래스 형태입니다. declare 키워드는 일반 파일 안에서 같은 일을 합니다. 컴파일러에게 "이것은 실행 시점에 존재하니 믿어 달라"고 말하며, 코드는 출력하지 않습니다.
선언이 문자열이라고 말하므로 컴파일러는 __APP_VERSION__.length를 받아들였습니다. 실행 시점에는 그런 변수가 없으므로 .length를 읽으면 ReferenceError: __APP_VERSION__ is not defined가 발생합니다. 컴파일러가 전혀 예상하지 못한 런타임 예외입니다. 이것이 모든 선언의 계약입니다. 타입은 그 뒤에 있는 JavaScript만큼만 참입니다.
.d.ts 파일에 들어가는 것
declaration: true로 컴파일해 보면 개념이 가장 잘 드러납니다. 다음 소스에서 시작합니다.
// price.ts
export interface LineItem {
name: string;
price: number;
qty: number;
}
const TAX = 0.2;
export function total(items: LineItem[]) {
const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
return Math.round(sum * (1 + TAX) * 100) / 100;
}
export class Cart {
private items: LineItem[] = [];
add(item: LineItem) {
this.items.push(item);
return this;
}
}
tsc는 price.js와 다음 price.d.ts를 씁니다.
export interface LineItem {
name: string;
price: number;
qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
private items;
add(item: LineItem): this;
}
함수 본문은 사라지고, 추론된 반환 타입(number, this)은 명시적으로 쓰이며, private 필드는 이름은 남지만 타입은 잃고, export하지 않은 TAX는 아예 없습니다. 라이브러리는 Node를 위한 .js와 에디터와 컴파일러를 위한 .d.ts를 함께 배포합니다. 번들러가 JavaScript를 빌드하는 프로젝트라면 emitDeclarationOnly: true로 .d.ts 파일만 만듭니다.
매일 쓰는 ES2022 내장 객체도 선언 파일에서 옵니다. lib.es2022.d.ts 같은 파일이 TypeScript와 함께 배포되며, target과 lib에 따라 선택됩니다.
declare의 여러 형태
모든 declare 문은 이미 존재하는 것을 기술합니다. import도 export도 없는 .d.ts 파일(전역 선언 파일)에서는 다음 각각이 프로젝트 전체에서 보입니다.
// globals.d.ts
declare const API_URL: string; // a global constant
declare let debugMode: boolean; // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget { // a class from a script tag
constructor(el: string);
render(): void;
}
declare namespace Analytics { // a global object
function page(name: string): void;
}
declare module "legacy-charts" { // a module you import
export function draw(data: number[]): void;
}
.d.ts 파일 안에서 interface와 type에는 declare가 필요 없습니다. 그 밖에 declare나 export 없는 최상위 선언은 TS1046 오류입니다. 파일에 import나 export가 생기면 그 선언들은 파일 안에서만 보이며, 전역 스코프에 추가하는 것은 declare global 블록(아래 참고)에 넣습니다. 컴파일러는 선언된 시그니처를 기준으로 호출을 검사합니다.
컴파일러는 index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'.를 보고합니다. { items: "3" }을 넘기거나, 실제 함수가 숫자를 받는다면 선언을 바꾸세요.
@types 패키지
많은 npm 패키지가 자체 .d.ts 파일을 배포하며, package.json의 types 필드(또는 exports의 types 조건)에서 참조합니다. JavaScript만 있는 패키지는 커뮤니티가 관리하는 DefinitelyTyped 프로젝트가 @types 스코프로 타입을 배포합니다.
npm i lodash
npm i -D @types/lodash
패키지를 import하면 TypeScript는 자동으로 node_modules/@types/{name}을 찾습니다. import가 아니라 전역을 기술하는 타입, 예를 들어 @types/node(process, Buffer)나 테스트 러너의 describe와 it은 tsconfig.json에 나열해야 합니다.
{
"compilerOptions": {
"types": ["node"]
}
}
이 항목이 없으면 TypeScript 7은 패키지가 설치되어 있어도 타입을 불러오지 않습니다.
error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
tsc --init은 새 설정에 "types": []를 쓰고, Node 프로젝트라면 ["node"]를 쓰라는 주석을 붙입니다.
타입이 없는 모듈에 타입 주기
번들된 타입도 @types도 없는 JavaScript 패키지를 import하면 TS7016 오류입니다.
error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.
최상위 import나 export가 없는 .d.ts 파일을 프로젝트 어딘가에 두어 고칩니다(types/ 폴더가 흔하며, include에 포함되기만 하면 됩니다). 사용하는 부분을 기술하세요.
// types/fakelib.d.ts
declare module "fakelib" {
export function hi(name: string): string;
export const version: string;
}
// Non-code files a bundler lets you import
declare module "*.svg" {
const url: string;
export default url;
}
가장 짧은 형태인 한 줄짜리 declare module "fakelib";은 패키지의 모든 import를 any로 만듭니다. 오류와 함께 모든 검사도 사라지므로 임시 조치로만 쓰세요.
declare global
폴리필, 내장 객체의 새 메서드, script 태그가 설정하는 전역 변수처럼 전역 스코프에 무언가를 추가하는 코드에는 그에 맞는 타입이 필요합니다. declare global이 이를 추가합니다. 모듈(import나 export가 있는 파일) 안에 있어야 하므로 맨 위에 export {}가 있습니다.
interface Array<T>는 내장 Array 인터페이스를 대체하지 않고 병합되며, globalThis에 프로퍼티를 추가하는 것은 (let이나 const가 아니라) var입니다. 공유 코드에서 내장 프로토타입을 확장하는 것은 위험합니다. 프로젝트가 @types/node의 NodeJS.ProcessEnv 인터페이스를 통해 process.env에 자신의 변수를 추가하는 것도 같은 declare global 기법입니다.
모듈 보강(Module Augmentation)
import하는 패키지의 타입에 무언가를 추가하려면 그 모듈 이름을 다시 선언하고 인터페이스를 다시 엽니다. 이 파일 자체가 모듈이어야 합니다(import가 그 역할을 하며, export {}도 됩니다). import나 export가 없으면 같은 블록이 완전히 새로운 config-lib 모듈을 선언해 패키지의 실제 타입을 가려 버립니다.
// types/config-lib.d.ts
import "config-lib";
declare module "config-lib" {
interface Settings {
beta: boolean; // merged into the package's own Settings interface
}
}
이렇게 하면 config-lib의 load().beta는 어디서든 boolean 타입이 됩니다. 인터페이스는 병합되지만 타입 별칭은 병합되지 않으므로, 라이브러리가 interface를 export해야 이 방법이 동작합니다. 플러그인이 프레임워크의 요청이나 설정 객체에 필드를 추가하는 방식이 바로 이것입니다.
skipLibCheck
skipLibCheck: true는 node_modules의 것을 포함해 .d.ts 파일의 타입 검사를 건너뜁니다. 자신의 코드는 여전히 그 선언들을 기준으로 검사됩니다. 시간을 절약하고 선언이 서로 충돌하는 두 패키지로 인한 오류를 피할 수 있어서 tsc --init이 이 옵션을 켭니다. 대신 자신의 .d.ts 파일 안에 있는 실수도 보고되지 않습니다.
자주 묻는 질문
TypeScript의 .d.ts 파일이란 무엇인가요?
선언 파일입니다. 구현 없이 타입(함수 시그니처, 인터페이스, 클래스 형태)만 담습니다. 컴파일된 라이브러리나 브라우저 내장 API처럼 다른 곳에 있는 JavaScript를 기술하여, 그것을 쓰는 코드를 TypeScript가 검사할 수 있게 합니다. tsc는 선언 파일에 대해 JavaScript를 출력하지 않습니다.
TypeScript에서 declare 키워드는 무엇을 하나요?
값을 만들지 않고, 그 값이 실행 시점에 존재한다고 컴파일러에게 알립니다. declare const VERSION: string;이라고 쓰면 VERSION을 문자열로 쓸 수 있고, 이 줄은 출력에서 사라집니다. 실제로 VERSION을 정의하는 것이 없으면 프로그램은 실행 시점에 ReferenceError로 실패합니다.
"Could not find a declaration file for module" 오류는 어떻게 고치나요?
TS7016 오류는 패키지에 타입이 없다는 뜻입니다. 타입이 있다면 설치하고(npm i -D @types/package-name), 없다면 사용하는 부분을 기술하는 declare module "package-name" { ... }를 .d.ts 파일에 추가하세요. declare module "package-name"; 한 줄만 쓰면 모듈 전체를 any로 만들어 오류를 없앱니다.
TypeScript에서 .d.ts 파일은 어떻게 생성하나요?
tsconfig.json에 "declaration": true를 설정하세요(또는 --declaration을 넘기세요). 그러면 모든 .ts 파일이 .js 옆에 .d.ts를 만듭니다. 다른 도구가 JavaScript를 빌드한다면 emitDeclarationOnly를, 에디터가 타입에서 소스로 이동할 수 있게 하려면 declarationMap을 추가하세요.
declare global과 declare module의 차이는 무엇인가요?
declare global { ... }은 Array의 새 프로퍼티나 전역 변수처럼 전역 스코프에 추가합니다. declare module "name" { ... }은 그 이름으로 import하는 모듈을 기술합니다. declare global은 모듈(import나 export가 있는 파일) 안에 있어야 합니다. declare module은 import도 export도 없는 파일에서는 새 모듈을 선언하고, 모듈 안에서는 기존 모듈을 보강합니다.