본문 바로가기

타입 선언 파일 작성 및 사용

타입이 없는 JavaScript 유틸리티의 함수·옵션·반환값을 d.ts로 선언해 프로젝트와 패키지 배포에서 함께 사용합니다.

앞서 8장 1절에서 .d.ts 파일이 무엇이고 왜 필요한지, 그리고 declare 키워드의 기본적인 사용법을 알아보았습니다.

이번 절에서는 실제 시나리오를 바탕으로 타입 선언 파일(.d.ts)을 어떻게 작성하고 프로젝트에서 사용하는지 구체적인 예시와 함께 살펴보겠습니다.


시나리오: 자바스크립트 유틸리티 라이브러리에 타입 추가하기

우리는 이미 존재하는 간단한 자바스크립트 유틸리티 파일이 있다고 가정하고, 이 파일에 타입스크립트의 타입 정보를 추가하는 .d.ts 파일을 작성해볼 것입니다.

자바스크립트 파일 (src/js/StringUtils.js)
src/js/StringUtils.js
// 이 파일은 CommonJS 모듈로 작성되었다고 가정합니다.

/**
 * 주어진 문자열의 첫 글자를 대문자로 변환합니다.
 * @param {string} str - 원본 문자열
 * @returns {string} 첫 글자가 대문자로 변환된 문자열
 */
function capitalize(str) {
  if (typeof str !== 'string' || str.length === 0) {
    return '';
  }
  return str.charAt(0).toUpperCase() + str.slice(1);
}

/**
 * 주어진 문자열이 비어있는지 (null, undefined, 빈 문자열) 확인합니다.
 * @param {string | null | undefined} str - 확인할 문자열
 * @returns {boolean} 비어있으면 true, 아니면 false
 */
function isEmpty(str) {
  return str === null || str === undefined || str.length === 0;
}

module.exports = {
  capitalize,
  isEmpty
};

이 자바스크립트 파일은 capitalize와 isEmpty라는 두 함수를 CommonJS 방식으로 내보냅니다.

이 파일에는 JSDoc이 있으므로 JavaScript 분석 설정에 따라 타입을 얻을 수도 있습니다. 여기서는 별도 .d.ts로 공개 계약을 제공하는 방법을 연습합니다.


타입 선언 파일 작성하기 (.d.ts)

이제 위 StringUtils.js 파일에 대한 타입 정보를 담은 StringUtils.d.ts 파일을 작성해봅시다.

로컬 sidecar는 JavaScript와 같은 폴더에 같은 기본 이름으로 둡니다. 다른 위치를 사용하려면 별도의 해석 연결이 필요합니다.

여기서는 src/js/StringUtils.d.ts로 생성하겠습니다.

타입 선언 파일 (src/js/StringUtils.d.ts)
src/js/StringUtils.d.ts
// StringUtils.js와 같은 폴더에 놓는 모듈 선언입니다.
// 실제 CommonJS 객체가 제공하는 두 함수를 최상위 export로 기술합니다.
/**
 * 주어진 문자열의 첫 글자를 대문자로 변환합니다.
 * @param str 원본 문자열
 * @returns 첫 글자가 대문자로 변환된 문자열
 */
export function capitalize(str: string): string;

/**
 * 주어진 문자열이 비어있는지 (null, undefined, 빈 문자열) 확인합니다.
 * @param str 확인할 문자열
 * @returns 비어있으면 true, 아니면 false
 */
export function isEmpty(str: string | null | undefined): boolean;
코드 분석

같은 이름의 sidecar: StringUtils.js 옆의 StringUtils.d.ts가 최상위 export로 공개 타입을 설명합니다. 상대 경로 이름의 전역 ambient module을 새로 선언하는 패턴이 아닙니다.

아래 확장자 생략 import는 CommonJS 출력과 해석을 전제로 합니다. Node.js ESM은 실행 파일 확장자까지 맞춰야 합니다.

export function capitalize(str: string): string;: 자바스크립트 파일에서 module.exports.capitalize로 내보냈던 capitalize 함수의 타입 시그니처를 선언합니다.

실제 구현 없이 오직 함수 시그니처만 작성합니다.

export 키워드를 사용하여 이 함수가 모듈 외부로 내보내진다는 것을 명시합니다.

export function isEmpty(str: string | null | undefined): boolean;: isEmpty 함수에도 동일하게 타입 시그니처를 선언합니다.

여기서는 str 매개변수가 string, null, undefined 중 하나일 수 있음을 유니온 타입으로 정확히 명시했습니다.

JSDoc 주석: JSDoc 주석(/** ... */)은 타입스크립트의 타입 정의 파일에서도 유용합니다.

IDE나 에디터에서 함수 사용 시 툴팁으로 표시되어 개발자에게 자세한 정보를 제공합니다.


타입 선언 파일 사용하기

이제 app.ts 파일에서 StringUtils 모듈을 가져와 타입 안전하게 사용해봅시다.

타입스크립트 파일 (src/app.ts)
src/app.ts
import { capitalize, isEmpty } from './js/StringUtils'; // .d.ts 파일에 의해 타입 정보가 제공됨

const myString = "hello world";
const capitalizedString = capitalize(myString);
console.log(capitalizedString); // "Hello world"

const emptyString = "";
const isStringEmpty = isEmpty(emptyString);
console.log(`"${emptyString}" is empty: ${isStringEmpty}`); // "" is empty: true

const nullString: string | null = null;
const isNullEmpty = isEmpty(nullString);
console.log(`null is empty: ${isNullEmpty}`); // null is empty: true

// 잘못된 타입의 인자 전달 시 컴파일 오류 발생
// const num = 123;
// const capitalizedNum = capitalize(num); // Error: 'number' 형식의 인수는 'string' 형식의 매개 변수에 할당될 수 없습니다.
컴파일러 동작

출력 폴더까지 연결하는 따라 하기

앞의 StringUtils.js, StringUtils.d.ts, app.ts를 표시된 src 경로에 그대로 저장합니다. TypeScript가 준비된 독립 폴더에서 package.json을 { "type": "commonjs" }로 두고 다음 구성을 추가합니다.

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "noEmitOnError": true,
    "types": []
  },
  "include": ["src/**/*.ts"]
}
tsc -p .
node -e "require('node:fs').mkdirSync('dist/js', { recursive: true }); require('node:fs').copyFileSync('src/js/StringUtils.js', 'dist/js/StringUtils.js')"
node dist/app.js

출력은 Hello world, "" is empty: true, null is empty: true 세 줄입니다. 여기서는 JavaScript를 컴파일 입력에 넣지 않았으므로 tsc가 StringUtils.js를 출력 폴더에 복사하지 않습니다. 출력 app.js의 require('./js/StringUtils')가 찾을 위치에 구현을 별도로 복사했습니다. 복사를 생략한 새 dist 폴더에서는 타입 검사는 통과해도 실행이 MODULE_NOT_FOUND로 실패합니다. 기존 dist에 파일이 남아 있다면 이 누락을 놓칠 수 있으므로 처음 실습한 폴더인지도 확인합니다.

입력과 실패 지점을 바꿔보기

원래 app.ts의 아래에 capitalize(''), isEmpty(' '), isEmpty(undefined)를 각각 출력하도록 추가하면 무엇이 나올까요? capitalize(123)을 주석 해제하면 타입 검사와 실제 JavaScript 함수의 방어 조건은 각각 어떤 역할을 하는지도 설명해 보세요.

구현의 분기와 선언의 제한 확인

빈 문자열의 대문자화 결과는 '', 공백 세 칸의 빈 값 검사는 false, undefined 검사는 true입니다. isEmpty는 길이가 0인지 검사하며 공백을 잘라내지 않습니다. 숫자 인자는 선언의 string에 맞지 않아 타입 진단이 납니다. 실제 JavaScript의 capitalize는 비문자열 입력에 ''를 반환하도록 방어하지만, 그 구현상의 허용 범위 전체를 공개 타입으로 허용한 것은 아닙니다.

아래 해석 단계는 이 예제에 같은 이름의 .tsx 등 다른 후보가 없는 경우를 설명합니다. 일반적인 .js 해석에는 .ts, .tsx, .d.ts 등 후보가 있을 수 있으며 선택한 해석 모드와 실행 환경을 함께 확인합니다. 공식 모듈 해석 참조는 타입 파일의 선택과 실행 파일의 선택을 구분합니다.

타입스크립트 컴파일러는 import { capitalize, isEmpty } from './js/StringUtils'; 구문을 만나면, 먼저 StringUtils.ts 파일을 찾습니다.

StringUtils.ts 파일이 없으면, StringUtils.d.ts 파일을 찾습니다.

StringUtils.d.ts가 선택되면 최상위로 내보낸 capitalize와 isEmpty의 시그니처를 사용합니다. 이는 타입 경로이며 실행 경로에는 실제 StringUtils.js가 필요합니다.

이 덕분에 capitalize(myString)와 같이 올바른 타입으로 함수를 호출하면 문제가 없지만, capitalize(num)처럼 잘못된 타입으로 호출하면 컴파일 타임에 오류를 잡아낼 수 있게 됩니다.

StringUtils의 로컬 타입 해석과 실행 해석은 분리되며 전역 및 패키지 선언에는 별도 포함 경로가 필요합니다.

TYPE / RUNTIME / CONTRACT

같은 import의 타입 경로와 실행 경로를 따로 읽는다

본문 app.ts는 ./js/StringUtils를 가져옵니다. 타입 검사는 같은 폴더의 .d.ts를 사용하고 CommonJS 실행은 실제 .js의 두 함수를 가져옵니다.

같은 import의 타입 경로와 실행 경로를 따로 읽는다StringUtils의 로컬 타입 해석과 실행 해석은 분리되며 전역 및 패키지 선언에는 별도 포함 경로가 필요합니다.
app.ts의 import타입 해석
연결 관계app.ts의 import · 타입 해석 → StringUtils.d.ts · 두 함수 시그니처
StringUtils.d.ts두 함수 시그니처
출력 JavaScriptCommonJS require
연결 관계출력 JavaScript · CommonJS require → StringUtils.js · 실제 두 함수
StringUtils.js실제 두 함수
같은 import의 타입 경로와 실행 경로를 따로 읽는다같은 가져오기 이름에 타입 해석과 실행 해석이 각각 있습니다. 출력 폴더를 쓸 때 실제 JS도 그 경로에 복사해야 하며 선언 파일이 구현을 출력하지 않습니다.
app.ts의 import타입 해석
타입 검사타입 검사app.ts의 import · 타입 해석 → StringUtils.d.ts · 두 함수 시그니처
StringUtils.d.ts두 함수 시그니처
출력 JavaScriptCommonJS require
실제 호출실제 호출출력 JavaScript · CommonJS require → StringUtils.js · 실제 두 함수
StringUtils.js실제 두 함수

같은 가져오기 이름에 타입 해석과 실행 해석이 각각 있습니다. 출력 폴더를 쓸 때 실제 JS도 그 경로에 복사해야 하며 선언 파일이 구현을 출력하지 않습니다.

LOCAL

파일 이름과 공개 export를 맞춘다

본문처럼 같은 기본 이름의 sidecar를 두면 선언이 타입 해석 대상이 됩니다. capitalize는 string 인자를 요구하고 isEmpty는 null·undefined도 허용합니다.

RUN

선언만 복사하면 실행 구현은 없다

출력 코드가 require하는 위치에 StringUtils.js가 필요합니다. Node ESM을 사용하면 package 형식과 실제 파일 확장자도 맞춰야 합니다.

GLOBAL

전역 선언은 프로젝트에 포함한다

global.d.ts의 MY_APP_NAME, VERSION_NUMBER, logActivity, Analytics와 Window 확장은 files/include 등으로 포함합니다. 실제 전역 구현은 먼저 로드해야 합니다.

PACKAGE

패키지 진입점은 메타데이터로 연결한다

my-awesome-library의 main은 dist/index.js, types는 dist/index.d.ts, files는 dist를 배포 대상으로 지정합니다. import한 모듈과 전역 포함은 같은 경로가 아닙니다.

상대 파일 해석은 설정과 확장자에 따라 후보가 달라집니다. 같은 이름의 최상위 export sidecar를 상대 경로 ambient module 선언으로 대체하지 않습니다.


전역 타입 선언 (.d.ts 파일의 또 다른 용도)

특정 자바스크립트 코드가 모듈 시스템을 사용하지 않고 전역 스코프에 변수나 함수를 선언하는 경우(예: <script> 태그로 로드되는 레거시 코드나 브라우저 API), declare 키워드를 최상위 레벨에서 사용하여 전역 타입을 선언할 수 있습니다.

전역 타입 선언 파일 (src/types/global.d.ts)
src/types/global.d.ts
// 전역 변수 선언
declare var MY_APP_NAME: string;
declare const VERSION_NUMBER: number;

// 전역 함수 선언
declare function logActivity(message: string, level: 'info' | 'warn' | 'error'): void;

// 전역 인터페이스 확장 (예: Window 객체에 새로운 속성 추가)
interface Window {
  myGlobalData?: {
    userId: number;
    sessionId: string;
  };
}

// 또는 declare namespace를 사용한 전역 객체 선언
declare namespace Analytics {
  function trackEvent(eventName: string, data?: object): void;
  function setUserId(id: string): void;
}
사용 예시 (src/main.ts)
src/main.ts
// global.d.ts가 프로젝트에 포함되고 실제 전역 구현도 로드되었다고 가정합니다.
console.log(`Application Name: ${MY_APP_NAME}`);
console.log(`Version: ${VERSION_NUMBER}`);

logActivity("User logged in", "info");

if (window.myGlobalData) {
  console.log(`User ID: ${window.myGlobalData.userId}`);
}

Analytics.trackEvent("page_view", { path: "/dashboard" });

// logActivity("Invalid level", "debug"); // Error: 'debug' 형식은 '"info" | "warn" | "error"' 형식에 할당될 수 없습니다.

이 전역 선언 파일을 tsconfig.json의 include 등에 포함하면 전역 타입 정보를 제공합니다. 런타임 값은 선언 파일이 생성하지 않으므로 별도 구현을 먼저 로드해야 합니다.


.d.ts 파일의 배포

직접 작성한 타입스크립트 라이브러리를 npm으로 배포할 때는, tsconfig.json의 declaration: true 옵션을 사용하여 컴파일 시 .js 파일과 함께 .d.ts 파일을 자동으로 생성하도록 설정합니다.

tsconfig.json
{
  "compilerOptions": {
    "declaration": true,      // .d.ts 파일 자동 생성
    "outDir": "./dist",       // .js 및 .d.ts 파일 출력 디렉토리
    // ...
  }
}

그리고 package.json 파일에 types (또는 typings) 필드를 추가하여, 라이브러리를 사용하는 다른 타입스크립트 프로젝트가 타입 정의 파일을 쉽게 찾을 수 있도록 경로를 명시합니다.

package.json
{
  "name": "my-awesome-library",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ]
}

main은 JavaScript 구현 파일, types는 타입 정의 파일을 가리키며, files는 배포할 dist 디렉터리를 지정합니다.

이 배포 구성은 별도의 TypeScript 라이브러리 예시입니다. 앞에서 손으로 작성한 StringUtils.d.ts를 declaration: true만으로 dist에 복사하는 절차는 아닙니다. 직접 작성한 선언과 JavaScript를 배포한다면 두 파일을 실제 출력 폴더에 포함시키고 대표 진입점의 파일 이름까지 맞춥니다. 생성할 소스가 TypeScript이면 선언 생성 설정을 사용하고, 직접 작성한 sidecar이면 별도 복사·패키지 구성으로 유지합니다.

이렇게 설정하면, npm install my-awesome-library를 통해 라이브러리를 설치한 사용자들은 자동으로 타입 정보를 받아 타입 안전하게 라이브러리를 사용할 수 있습니다.


.d.ts 파일의 작성과 사용은 타입스크립트가 자바스크립트 생태계와 효과적으로 상호작용하는 핵심 방법입니다.

기존 자바스크립트 코드에 타입을 부여하거나 타입스크립트 라이브러리를 배포할 때, .d.ts 작성법을 능숙하게 다루는 것이 중요합니다.

declare module, export, 정확한 타입 시그니처 작성을 통해 코드 타입 안정성과 개발 경험을 크게 향상시킬 수 있습니다.

실무에서는 선언 파일을 작성한 뒤 실제 import 경로, 컴파일러 포함 범위, 패키지 메타데이터까지 이어서 확인해야 합니다.

아래 다이어그램은 로컬 작성부터 배포까지 .d.ts가 끊기지 않게 연결되는 흐름을 보여줍니다.

StringUtils.js 구현, StringUtils.d.ts 계약, app.ts 사용과 배포 시 경로 연결을 설명합니다.

TYPE / RUNTIME / CONTRACT

StringUtils의 실제 동작을 선언하고 소비 코드로 확인한다

기존 CommonJS 파일의 두 함수를 관찰해 같은 이름의 .d.ts를 작성합니다. 올바른 호출·잘못된 인자·실제 결과를 함께 확인하고 API 변경 때 구현과 선언을 갱신합니다.

StringUtils의 실제 동작을 선언하고 소비 코드로 확인한다원 구현의 공개 API를 사람이 sidecar에 기술합니다. app은 선언으로 타입을 검사하고 실제 JS를 호출합니다. 두 확인은 별도 경로이며 직접 작성한 선언을 자동 생성 결과와 혼동하지 않습니다.API 기술타입실제 JavaScript 호출StringUtils.jsmodule.exportsStringUtils.d.ts공개 시그니처app.ts호출과 결과 확인
StringUtils의 실제 동작을 선언하고 소비 코드로 확인한다원 구현의 공개 API를 사람이 sidecar에 기술합니다. app은 선언으로 타입을 검사하고 실제 JS를 호출합니다. 두 확인은 별도 경로이며 직접 작성한 선언을 자동 생성 결과와 혼동하지 않습니다.API 기술타입 검사실제 호출StringUtils.jsmodule.exportsStringUtils.d.ts공개 시그니처app.ts호출과 결과 확인

원 구현의 공개 API를 사람이 sidecar에 기술합니다. app은 선언으로 타입을 검사하고 실제 JS를 호출합니다. 두 확인은 별도 경로이며 직접 작성한 선언을 자동 생성 결과와 혼동하지 않습니다.

1 · IMPLEMENTATION

실제 두 함수의 계약을 읽는다

StringUtils.js는 module.exports로 capitalize와 isEmpty를 내보냅니다. capitalize는 첫 글자를 대문자로 바꾸고 isEmpty는 null·undefined·빈 문자열을 검사합니다.

2 · SIDECAR

같은 폴더에 공개 시그니처를 쓴다

src/js/StringUtils.d.ts의 최상위 export로 capitalize(string):string과 isEmpty(string|null|undefined):boolean을 기술합니다. 함수 구현을 다시 작성하지 않습니다.

3 · VERIFY

타입과 실제 결과를 확인한다

app.ts의 출력은 Hello world, 빈 문자열 검사 true, null 검사 true입니다. capitalize(123)은 선언을 사용하는 타입 검사에서 오류입니다. 실행은 실제 .js를 필요로 합니다.

4 · MAINTAIN

작성 방식에 맞춰 갱신하고 배포한다

이 로컬 JavaScript sidecar는 직접 갱신합니다. 별도 TypeScript 라이브러리는 declaration으로 생성하고 package main/types와 배포 파일을 맞춥니다. 두 방식을 같은 생성 단계로 합치지 않습니다.

JavaScript의 JSDoc으로 타입을 얻는 방법도 있습니다. 여기서는 구현 옆의 직접 작성한 .d.ts로 공개 계약을 제공하는 예제입니다.

선언 파일은 한 번 작성하고 끝나는 파일이 아니라 런타임 API가 바뀔 때마다 사용 지점, 빌드 출력, 패키지 메타데이터와 함께 갱신해야 하는 계약입니다.


타입 선언 파일 작성 및 사용을 코드에 적용하기 전, 컴파일 오류가 막아 줄 지점과 사람이 약속해야 할 지점을 나눕니다.