8.2 선언 파일(.d.ts) — 타입 정의의 세계
선언 파일이란?
.d.ts 파일은 타입 정보만 담은 파일입니다. 실제 구현 코드는 없고, TypeScript 컴파일러에게 "이 값의 타입은 이렇다"고 알려주는 역할을 합니다.
// math.d.ts — 선언 파일 (구현 없음)
export declare function add(a: number, b: number): number;
export declare function subtract(a: number, b: number): number;
export declare const PI: number;
// math.js — 실제 구현 (타입 없음)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export const PI = 3.14159;
TypeScript는 .js + .d.ts 쌍을 보고 타입 안전하게 JavaScript 라이브러리를 사용할 수 있습니다.
.d.ts 파일이 필요한 이유
| 상황 | 설명 |
|---|---|
| JS 라이브러리 사용 | lodash, jquery 등은 JS로 작성됨 |
| 빌드 결과물 타입 제공 | 라이브러리 배포 시 .js + .d.ts 함께 제공 |
| 성능 최적화 | skipLibCheck와 함께 타입 체크 속도 향상 |
tsc로 .d.ts 자동 생성
// tsconfig.json
{
"compilerOptions": {
"declaration": true, // .d.ts 파일 생성
"declarationMap": true, // .d.ts.map 소스맵 생성
"declarationDir": "./types", // .d.ts 출력 디렉토리 (선택)
"emitDeclarationOnly": true // .d.ts만 생성 (번들러가 JS 처리 시)
}
}
// src/utils.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export interface Config {
timeout: number;
retries: number;
}
컴파일 결과:
// dist/utils.d.ts (자동 생성)
export declare function greet(name: string): string;
export interface Config {
timeout: number;
retries: number;
}
@types/ 패키지 — DefinitelyTyped
JavaScript 라이브러리 중 TypeScript 타입 정의가 내장되지 않은 경우, @types/ 패키지를 별도로 설치합니다.
npm install --save-dev @types/node # Node.js 타입
npm install --save-dev @types/express # Express 타입
npm install --save-dev @types/lodash # lodash 타입
npm install --save-dev @types/jest # Jest 타입
설치 여부 확인 방법
import express from 'express';
// 빨간 밑줄이 뜨면 @types/express 설치 필요
// 또는 라이브러리에 자체 타입 정의 내장됨 (예: axios)
타입 내장 라이브러리 vs @types 필요 라이브러리
// 타입 내장 (별도 설치 불필요)
import axios from 'axios'; // axios: package.json에 "types" 필드 포함
import { z } from 'zod'; // zod: TypeScript로 작성됨
import { PrismaClient } from '@prisma/client';
// @types 필요
import express from 'express'; // @types/express 필요
import _ from 'lodash'; // @types/lodash 필요
import fs from 'fs'; // @types/node에 포함
선언 파일 직접 작성하기
함수 선언
// globals.d.ts
declare function fetchUser(id: number): Promise<User>;
declare function formatDate(date: Date, format: string): string;
변수/상수 선언
declare const API_URL: string;
declare let currentUser: User | null;
declare var __DEV__: boolean;
인터페이스와 타입 선언
// types.d.ts
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
클래스 선언
declare class EventEmitter {
on(event: string, listener: (...args: any[]) => void): this;
off(event: string, listener: (...args: any[]) => void): this;
emit(event: string, ...args: any[]): boolean;
}
모듈 선언 파일 작성
기존 JavaScript 라이브러리에 타입을 추가할 때 사용합니다.
// legacy-lib.d.ts
declare module 'legacy-lib' {
export function initialize(config: LegacyConfig): void;
export function process(data: string): string;
interface LegacyConfig {
apiKey: string;
timeout?: number;
debug?: boolean;
}
}
// 사용 시
import { initialize, process } from 'legacy-lib';
initialize({ apiKey: 'abc123' }); // ✅ 타입 안전
실전 예제: 라이브러리에 .d.ts 제공하기
my-utils/
├── src/
│ ├── string.ts
│ └── array.ts
├── dist/
│ ├── string.js
│ ├── string.d.ts
│ ├── array.js
│ └── array.d.ts
├── package.json
└── tsconfig.json
// package.json
{
"name": "my-utils",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts", // 타입 진입점
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts" // ESM 타입 진입점
}
}
}
// src/string.ts
/**
* 문자열의 첫 글자를 대문자로 변환
*/
export function capitalize(str: string): string {
if (!str) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* 문자열을 camelCase로 변환
*/
export function toCamelCase(str: string): string {
return str
.split(/[-_\s]+/)
.map((word, i) => i === 0 ? word.toLowerCase() : capitalize(word))
.join('');
}
컴파일 후:
// dist/string.d.ts (자동 생성)
/**
* 문자열의 첫 글자를 대문자로 변환
*/
export declare function capitalize(str: string): string;
/**
* 문자열을 camelCase로 변환
*/
export declare function toCamelCase(str: string): string;
JSDoc이 .d.ts에도 보존됩니다. IDE에서 마우스를 올리면 문서가 표시됩니다.
DefinitelyTyped에 기여하기
DefinitelyTyped는 커뮤니티가 관리하는 TypeScript 타입 정의 저장소입니다.
기여 절차
- 저장소 fork
git clone https://github.com/DefinitelyTyped/DefinitelyTyped
cd DefinitelyTyped
- 타입 파일 작성
types/
└── my-library/
├── index.d.ts # 타입 정의
├── tsconfig.json # 타입 체크 설정
└── my-library-tests.ts # 타입 테스트
- index.d.ts 작성 규칙
// types/my-library/index.d.ts
// 헤더 주석 (필수)
// Type definitions for my-library 1.0
// Project: https://github.com/author/my-library
// Definitions by: Your Name <https://github.com/yourname>
export interface Options {
timeout?: number;
retries?: number;
}
export function request(url: string, options?: Options): Promise<Response>;
- 타입 테스트 작성
// my-library-tests.ts
import { request, Options } from 'my-library';
// $ExpectType 주석으로 타입 검증
const opts: Options = { timeout: 5000 };
const result = request('https://api.example.com', opts);
// $ExpectType Promise<Response>
- PR 제출 → 리뷰 후
@types/my-library패키지로 배포
고수 팁
1. typeRoots로 타입 탐색 경로 지정
{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types", // 기본값
"./types" // 커스텀 타입 디렉토리
]
}
}
2. types로 사용할 @types 패키지 제한
{
"compilerOptions": {
"types": ["node", "jest"] // 이 두 개만 전역 타입으로 포함
}
}
3. 인라인 타입 선언 오버라이드
// src/types/express/index.d.ts
import 'express';
declare module 'express' {
interface Request {
user?: AuthUser; // express의 Request 타입 확장
}
}
4. skipLibCheck: true로 성능 최적화
{
"compilerOptions": {
"skipLibCheck": true // .d.ts 파일 타입 체크 건너뜀 (빌드 속도↑)
}
}