본문 바로가기

.d.ts 파일 작성법

구현이 없는 d.ts 파일에 declare로 JavaScript 변수·함수·클래스의 공개 타입을 기술하고 컴파일러가 찾도록 배치합니다.

타입스크립트는 타입 추론을 통해 많은 타입 정보를 자동으로 얻어낼 수 있지만, 자바스크립트 코드(라이브러리, 프레임워크, 레거시 코드 등)를 타입스크립트 프로젝트에서 사용할 때는 타입 정보를 알 수 없는 경우가 발생합니다.

이럴 때 필요한 것이 바로 타입 선언 파일(Declaration Files), 즉 .d.ts 파일입니다.

.d.ts 파일은 실제 구현 코드 없이, 오직 타입 정보만을 담고 있는 파일입니다.

자바스크립트 코드가 외부에 공개하는 변수, 함수, 클래스 등의 설명서 역할을 하며, 타입스크립트 컴파일러는 이 .d.ts 파일을 참조하여 해당 자바스크립트 코드의 타입을 정확하게 이해하고 타입 검사를 수행합니다.


왜 .d.ts 파일이 필요한가?

선언은 정적 타입을 제공하며 실제 런타임 값을 생성하거나 응답을 검증하지 않습니다.

TYPE / RUNTIME / CONTRACT

declare는 기존 값을 설명하고 구현은 따로 제공한다

선언 파일은 코드가 사용하는 이름과 타입을 알려 줍니다. 변수·함수·클래스·enum의 실제 값과 동작은 JavaScript나 외부 실행 환경이 제공해야 합니다.

declare는 기존 값을 설명하고 구현은 따로 제공한다선언은 정적 타입을 제공하며 실제 런타임 값을 생성하거나 응답을 검증하지 않습니다.
.d.ts 선언이름 · 호출 계약
연결 관계.d.ts 선언 · 이름 · 호출 계약 → TypeScript 검사 · 정적 사용 검사
TypeScript 검사정적 사용 검사
JavaScript / 환경실제 구현
연결 관계JavaScript / 환경 · 실제 구현 → 실행 · 실제 값 · 동작
실행실제 값 · 동작
declare는 기존 값을 설명하고 구현은 따로 제공한다위는 선언을 읽는 타입 검사 경로, 아래는 실제 값을 제공하는 실행 경로입니다. 두 경로 사이에 값을 생성한다는 간선은 없습니다.
.d.ts 선언이름 · 호출 계약
타입 검사타입 검사.d.ts 선언 · 이름 · 호출 계약 → TypeScript 검사 · 정적 사용 검사
TypeScript 검사정적 사용 검사
JavaScript / 환경실제 구현
실제 호출실제 호출JavaScript / 환경 · 실제 구현 → 실행 · 실제 값 · 동작
실행실제 값 · 동작

위는 선언을 읽는 타입 검사 경로, 아래는 실제 값을 제공하는 실행 경로입니다. 두 경로 사이에 값을 생성한다는 간선은 없습니다.

GLOBAL

MY_GLOBAL_CONFIG는 만들어지지 않는다

apiUrl:string과 timeout:number는 사용 코드의 타입 약속입니다. 실제 전역 객체가 로드되지 않으면 해당 이름을 읽는 실행 코드가 실패할 수 있습니다.

FUNCTION / CLASS

호출과 생성의 표면을 설명한다

customLog와 calculateSum, MyEventEmitter<T>의 on/emit은 구현을 포함하지 않습니다. new로 사용할 클래스도 실제 생성자가 필요합니다.

ENUM / NAMESPACE

선언한 객체도 실제 객체가 필요하다

StatusCode.Success=200 등의 ambient enum은 실행 객체를 만들지 않습니다. jQuery.ajax나 $도 라이브러리가 실제로 제공해야 합니다.

MODULE / ASSET

import 대상과 로더를 함께 맞춘다

my-custom-module의 Client와 getData는 실제 패키지 구현이 필요합니다. *.png의 string URL과 *.svg의 URL·React 컴포넌트도 해당 로더가 제공해야 합니다.

선언과 실제 API가 어긋나면 사용 코드가 타입 검사를 통과해도 실행이 실패할 수 있습니다. 선언의 타입 인자는 외부 데이터의 런타임 검증을 대신하지 않습니다.

.d.ts 파일이 필요한 주요 이유는 다음과 같습니다.

자바스크립트 라이브러리 사용: jQuery, Lodash, React 등 수많은 자바스크립트 라이브러리들은 .js 파일 형태로 배포됩니다.

자체 선언이나 별도 타입 정보가 없다면 설정에 따라 any가 되거나 선언 누락 오류가 납니다. JavaScript를 분석 대상으로 포함하면 JSDoc과 코드에서 타입 정보를 얻을 수도 있습니다.

레거시 자바스크립트 코드 연동: 기존에 작성된 자바스크립트 코드를 타입스크립트 프로젝트에 통합할 때, 명시적인 타입 정보를 제공하여 타입 안전성을 확보합니다.

타입스크립트 라이브러리 배포: 타입스크립트 라이브러리에서 declaration 생성을 설정하면 JavaScript 구현과 함께 .d.ts를 배포할 수 있습니다.

사용자는 .js 파일(실제 구현)과 .d.ts 파일(타입 정보)을 함께 받아 타입스크립트 환경에서 라이브러리를 안전하게 사용할 수 있습니다.

전역 변수/객체 선언: 브라우저 환경에서 window, document와 같이 전역 스코프에 존재하는 객체나 변수의 타입을 명시할 때 사용됩니다.


.d.ts 파일의 기본 구조와 declare 키워드

.d.ts 파일은 .ts 파일과 거의 동일한 문법을 사용하지만, declare 키워드를 사용하여 실제 구현 없이 타입 정보만 선언한다는 점을 명시합니다.

declare 키워드는 컴파일된 자바스크립트 코드에는 포함되지 않습니다.

주요 declare 사용 예시

값을 준비한 뒤 선언으로 읽어보기

아래 각 선언은 용도를 설명하는 별개 예시입니다. 전부 한 파일에 붙이지 않습니다. 특히 뒤의 jQuery 선언은 JQueryPromise<any> 등 다른 라이브러리 타입에 의존하는 발췌이며, 이 절만 복사해서 완성하는 독립 구현은 아닙니다.

다음 작은 실습은 앞의 개념을 실제 파일 세 개로 연결합니다. TypeScript가 준비된 독립 폴더에서 진행하며 package.json은 { "type": "commonjs" }로 둡니다. 아래의 custom-globals.d.ts에는 다음에 나오는 원래 선언 코드만 저장합니다.

src/runtime.js
globalThis.MY_GLOBAL_CONFIG = { apiUrl: '/api', timeout: 1000 };
globalThis.globalCounter = 0;
src/demo.ts
console.log(MY_GLOBAL_CONFIG.apiUrl);
console.log(MY_GLOBAL_CONFIG.timeout);
globalCounter += 1;
console.log(globalCounter);
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noEmitOnError": true,
    "types": []
  },
  "files": ["src/custom-globals.d.ts", "src/demo.ts"]
}
tsc -p .
node --require ./src/runtime.js dist/demo.js

출력은 /api, 1000, 1 세 줄입니다. Node가 먼저 실행한 runtime.js는 실제 값을 만들고, 컴파일러가 읽은 선언은 demo.ts에서 그 이름의 사용을 검사합니다. 선언 파일은 출력 JavaScript에 전역 객체를 만드는 명령을 추가하지 않습니다. node dist/demo.js만 실행해 값을 제공하지 않으면 첫 접근에서 ReferenceError가 납니다.

선언과 실행을 따로 판단하기

실제 설정의 timeout을 2500, 시작 globalCounter를 5로 바꾸면 두 번째와 세 번째 출력은 무엇일까요? 선언 파일만 남기고 runtime.js를 로드하지 않으면 타입 검사 성공만으로 실행도 성공할까요? 실제 객체의 timeout을 문자열 '1000'으로 잘못 제공한 경우도 정적 선언이 이를 자동으로 숫자로 바꾸는지 생각해 보세요.

전역 값과 설명의 역할 확인

출력은 2500, 6입니다. 선언이 프로젝트에 포함되면 이름의 정적 타입을 알 수 있지만 실행 값은 별도로 필요합니다. 잘못 제공한 문자열을 선언 파일이 변환하거나 검사하지 않으므로 실제 구현과 선언을 함께 맞춰야 합니다. 설정처럼 외부에서 오는 값은 로드하는 경계에서 검증하는 절차도 필요합니다.

전역 변수·함수·클래스의 선언 형태는 공식 선언 참조에서 대조할 수 있습니다.

declare var / declare let / declare const: 전역 변수를 선언합니다.

custom-globals.d.ts
declare const MY_GLOBAL_CONFIG: {
  apiUrl: string;
  timeout: number;
};

declare let globalCounter: number;

declare function: 전역 함수를 선언합니다.

global-funcs.d.ts
declare function customLog(message: string): void;
declare function calculateSum(...numbers: number[]): number;

declare class: 클래스를 선언합니다.

custom-class.d.ts
declare class MyEventEmitter<T> {
  on(eventName: string, listener: (data: T) => void): void;
  emit(eventName: string, data: T): void;
}

declare enum: 다른 곳에서 제공하는 열거형 값의 타입을 설명합니다. 아래 일반 ambient enum은 멤버에 접근할 런타임 객체가 따로 필요하며 const enum과 같지 않습니다.

status-codes.d.ts
declare enum StatusCode {
  Success = 200,
  NotFound = 404,
  ServerError = 500
}

declare namespace: 관련된 전역 변수, 함수, 클래스 등을 하나의 논리적인 그룹으로 묶습니다. (7장 3절 "네임스페이스" 참조)

jquery.d.ts (예시)
declare namespace jQuery {
  interface Event {
    // ...
  }
  function ajax(url: string, settings?: any): JQueryPromise<any>;
  const version: string;
}

declare var $: typeof jQuery; // $도 jQuery 네임스페이스와 동일한 타입을 가짐

이 예시처럼 .d.ts 파일에서 declare namespace는 전역 스코프에 특정 이름을 가진 객체가 존재하며 그 안에 멤버들이 있음을 선언합니다.

declare module: 특정 경로 또는 모듈 이름을 가진 자바스크립트 모듈의 타입을 선언합니다.

  • ES 모듈/CommonJS 모듈의 타입 선언
    my-custom-module.d.ts
    declare module 'my-custom-module' {
      export interface Options {
        debug: boolean;
        port: number;
      }
      export function init(options: Options): void;
      export const version: string;
      export default class Client { // default export
        constructor(apiUrl: string);
        getData(): Promise<any>;
      }
    }

    이 선언이 프로젝트에 포함되면 해당 import의 타입을 검사할 수 있습니다. 실제 실행에는 같은 이름과 export를 제공하는 모듈 구현이 별도로 필요합니다.

  • 파일 확장자를 위한 모듈 선언: 웹팩과 같은 번들러를 사용할 때 .css, .png 등 자바스크립트가 아닌 파일을 import하는 경우가 있습니다. 이들에 대한 타입을 선언할 때도 declare module을 사용합니다.

    image.d.ts
    declare module '*.png' {
      const content: string; // 이미지 파일은 URL 문자열로 로드된다고 가정
      export default content;
    }
    
    declare module '*.svg' {
      import * as React from 'react';
      export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement> & { title?: string }>;
      const src: string;
      export default src;
    }

    선언이 포함되면 이미지 import의 타입을 설명할 수 있습니다. PNG의 URL과 SVG의 ReactComponent를 실제로 제공하는 번들러·loader 설정은 별도로 필요합니다.


.d.ts 파일 작성 팁

필요한 타입만 선언: .d.ts 파일은 실제 구현 코드를 포함하지 않으므로, 자바스크립트 코드가 외부에 노출하는 멤버들만 선언하면 됩니다.

내부적으로만 사용되는 변수나 함수는 선언할 필요가 없습니다.

가능한 한 구체적인 타입 사용: any 타입을 남용하지 않고, 가능한 한 정확하고 구체적인 타입을 사용하여 타입 안전성을 극대화해야 합니다.

제네릭, 유니온 타입, 인터섹션 타입 등을 적극적으로 활용하세요.

불필요한 export 피하기: .d.ts 파일이 모듈이 아닌 전역 타입 선언을 목적으로 한다면, 최상위 레벨에서 export를 사용하지 않아야 합니다.

export를 사용하면 해당 .d.ts 파일은 모듈로 간주됩니다.

  • 전역 선언: declare var $, declare function jQuery()
  • 모듈 선언: declare module 'lodash' { export function ... }

자동 생성 활용: tsc --declaration 또는 tsc -d 옵션을 사용하면 타입스크립트 소스 코드로부터 .d.ts 파일을 자동으로 생성할 수 있습니다.

자동 생성한 선언을 수정해야 한다면 원본 TypeScript API나 생성 설정을 고친 뒤 다시 생성합니다. JavaScript용으로 직접 작성한 선언은 실제 구현과 함께 갱신합니다.

DefinitelyTyped 참조: 널리 사용되는 자바스크립트 라이브러리들은 DefinitelyTyped 프로젝트에서 타입 정의를 제공하며, 이는 @types/ 접두사가 붙은 npm 패키지로 배포됩니다 (예: npm install @types/lodash).

새롭게 .d.ts 파일을 작성하기 전에, 사용할 라이브러리 타입 정의가 이미 존재하는지 먼저 확인하는 것이 좋습니다.


.d.ts 파일의 위치와 컴파일러 설정

.d.ts는 files·include, 모듈 import 해석, 타입 패키지 포함 규칙 등에 따라 프로젝트에 들어옵니다. 단순히 임의의 위치에 두는 것만으로 모든 선언이 자동 포함되지는 않습니다.

  • tsconfig.json의 include: tsconfig.json 파일의 include 배열에 .d.ts 파일이 포함된 디렉터리를 지정합니다.

  • 패키지 내 types 필드: npm 패키지의 package.json 파일에 types 또는 typings 필드를 사용하여 주된 타입 정의 파일의 경로를 명시할 수 있습니다.

    package.json
    {
      "name": "my-cool-lib",
      "version": "1.0.0",
      "main": "dist/index.js",
      "types": "dist/index.d.ts"
    }

    types 필드는 타입스크립트가 패키지의 대표 타입 정의 파일을 찾을 경로를 지정합니다.

  • typeRoots 및 types: tsconfig.json의 typeRoots 옵션은 타입 정의 파일을 검색할 루트 디렉터리를 지정하며, types 옵션은 전역적으로 포함할 @types 패키지를 명시합니다.


.d.ts 파일은 자바스크립트 생태계와의 상호 운용성을 확보하고 타입스크립트의 타입 안전성이라는 핵심 가치를 유지하는 데 필수적인 요소입니다.

자바스크립트 라이브러리를 사용할 때, 레거시 코드를 통합할 때, 또는 직접 타입스크립트 라이브러리를 개발할 때 .d.ts 파일을 올바르게 작성하고 활용하는 방법을 숙지하는 것이 중요합니다.

작성할 선언 파일의 형태를 고르기 전에는 “값은 이미 존재하고, 우리는 타입만 설명한다”는 기준을 먼저 세워야 합니다.

다음 흐름은 소스 API에서 생성 선언과 패키지 메타데이터를 거쳐 소비자 검증으로 이어지는 배포 절차입니다.

소스 API의 JavaScript 출력과 선언 출력, 패키지 경로 연결 및 소비자 확인을 구분합니다.

TYPE / RUNTIME / CONTRACT

소스의 공개 API를 실행 파일과 선언 파일로 배포한다

TypeScript 소스는 JavaScript 구현과 .d.ts 타입 설명을 출력할 수 있습니다. 패키지는 두 파일의 경로를 각각 연결하며 소비자는 타입 검사와 실행을 따로 확인합니다.

소스의 공개 API를 실행 파일과 선언 파일로 배포한다소스 API의 JavaScript 출력과 선언 출력, 패키지 경로 연결 및 소비자 확인을 구분합니다.TypeScript 소스공개 APIJavaScript 출력.js 구현선언 출력.d.ts 계약
소스의 공개 API를 실행 파일과 선언 파일로 배포한다같은 공개 API에서 구현과 선언을 각각 출력합니다. 분기는 동시에 준비할 두 결과를 뜻하며 .js가 자동으로 .d.ts의 정확성을 입증한다는 순서가 아닙니다. 패키지 경로와 소비자 확인은 아래 설명에서 이어집니다.TypeScript 소스공개 APIJS 출력.js 구현선언 출력.d.ts 계약

같은 공개 API에서 구현과 선언을 각각 출력합니다. 분기는 동시에 준비할 두 결과를 뜻하며 .js가 자동으로 .d.ts의 정확성을 입증한다는 순서가 아닙니다. 패키지 경로와 소비자 확인은 아래 설명에서 이어집니다.

1 · SOURCE

공개 API를 소스에서 정한다

내보낼 함수·클래스·타입을 정의합니다. 생성된 선언을 바꿔야 하면 원본 API를 수정하고 다시 생성합니다.

2 · OUTPUT

구현과 타입 설명을 함께 준비한다

declaration:true는 .d.ts 생성을 켭니다. declarationMap은 선택 옵션입니다. 손으로 작성한 JavaScript용 선언은 구현과 함께 갱신합니다.

3 · PACKAGE

두 경로를 각각 연결한다

본문 my-cool-lib의 main은 dist/index.js, types는 dist/index.d.ts입니다. 실제 배포 파일에 두 경로의 대상이 존재해야 합니다.

4 · CONSUMER

타입과 실행을 따로 확인한다

소비 코드가 선언을 찾고 올바른 호출을 허용하는지 확인합니다. 실제 import와 호출 결과도 확인해야 하며 타입 통과만으로 구현의 동작을 증명할 수 없습니다.

선언 생성과 타입 검사의 성공은 구분합니다. 오류가 있을 때 출력을 막으려면 noEmitOnError 등 빌드 설정도 확인합니다.

선언 파일을 추가할 때는 타입이 맞는지만 보는 것이 아니라 값의 출처, 스코프, 배포 경로가 서로 맞물리는지도 확인해야 합니다.

다음 절에서는 실제 JavaScript 유틸리티에 선언 파일을 작성하고 사용하는 과정을 살펴보겠습니다.