모듈 `.d.ts` 작성법
모듈 .d.ts 작성법 (Modules .d.ts)
모듈 하나를 위한 타입 정의를 처음 쓴다면, 어디서부터 시작해야 할지가 늘 첫 고민이에요. 이 문서는 실제 자바스크립트 코드를 .d.ts로 옮길 때 자주 마주치는 CommonJS 패턴들을 하나씩 짚어주는 가이드입니다. 코드는 그대로 보존하되, "이게 왜 이렇게 선언되는지"를 옆에서 설명해 드릴게요.
출처: TypeScript 핸드북
자바스크립트와 .d.ts 예시 비교
흔한 CommonJS 패턴들
CommonJS 패턴을 쓰는 모듈은 module.exports로 내보내는 값을 표현해요. 예를 들어, 함수 하나와 숫자 상수 하나를 내보내는 모듈을 볼게요.
const maxInterval = 12;
function getArrayLength(arr) {
return arr.length;
}
module.exports = {
getArrayLength,
maxInterval,
};
이 모듈은 다음과 같은 .d.ts로 표현할 수 있어요.
export function getArrayLength(arr: any[]): number;
export const maxInterval: 12;
TypeScript 플레이그라운드는 자바스크립트 코드에 해당하는 .d.ts를 직접 보여줍니다. 이 링크에서 직접 확인해 보세요.
.d.ts 문법은 의도적으로 ES Modules 문법처럼 보이게 만들어졌어요. ES Modules는 TC39에서 2015년 ES2015(ES6)의 일부로 비준됐고, 그 전부터 트랜스파일러를 통해 오랫동안 사용돼 왔죠. 자바스크립트 코드베이스가 ES Modules를 쓴다면, 이런 함수는:
export function getArrayLength(arr) {
return arr.length;
}
다음과 같은 .d.ts가 됩니다.
export function getArrayLength(arr: any[]): number;
기본 내보내기 (Default Exports)
CommonJS에서는 어떤 값이든 기본 내보내기로 내보낼 수 있어요. 예를 들어 정규 표현식을 내보내는 모듈을 볼게요.
module.exports = /hello( world)?/;
이는 다음과 같은 .d.ts로 표현할 수 있어요.
declare const helloWorld: RegExp;
export = helloWorld;
숫자를 내보낸다면:
module.exports = 3.142;
declare const pi: number;
export = pi;
CommonJS에서 자주 쓰는 내보내기 방식 중 하나가 함수를 내보내는 거예요. 함수는 객체이기도 하기 때문에, 추가 필드를 붙일 수 있고 그 필드도 내보내기에 포함됩니다.
function getArrayLength(arr) {
return arr.length;
}
getArrayLength.maxInterval = 12;
module.exports = getArrayLength;
이건 이렇게 표현할 수 있어요.
declare function getArrayLength(arr: any[]): number;
declare namespace getArrayLength {
declare const maxInterval: 12;
}
export = getArrayLength;
이게 어떻게 동작하는지는 모듈: 함수 (Module: Functions)와 Modules 참고 문서를 참고하세요.
다양한 소비자 import 처리하기
요즘 소비 코드에서는 모듈을 import 하는 방식이 정말 다양합니다.
const fastify = require("fastify");
const { fastify } = require("fastify");
import fastify = require("fastify");
import * as Fastify from "fastify";
import { fastify, FastifyInstance } from "fastify";
import fastify from "fastify";
import fastify, { FastifyInstance } from "fastify";
이 모든 경우를 다 커버하려면 자바스크립트 코드가 실제로 그 패턴들을 전부 지원해야 해요. 이런 패턴들을 많이 지원하려면 CommonJS 모듈은 대략 이렇게 생겨야 합니다.
class FastifyInstance {}
function fastify() {
return new FastifyInstance();
}
fastify.FastifyInstance = FastifyInstance;
// Allows for { fastify }
fastify.fastify = fastify;
// Allows for strict ES Module support
fastify.default = fastify;
// Sets the default export
module.exports = fastify;
모듈 안의 타입
자바스크립트 코드에는 존재하지 않는 타입을 제공하고 싶을 때가 있어요. 다음 모듈을 볼게요.
function getArrayMetadata(arr) {
return {
length: getArrayLength(arr),
firstObject: arr[0],
};
}
module.exports = {
getArrayMetadata,
};
이건 이렇게 표현할 수 있어요.
export type ArrayMetadata = {
length: number;
firstObject: any | undefined;
};
export function getArrayMetadata(arr: any[]): ArrayMetadata;
이 예시는 제네릭 사용이 더 풍부한 타입 정보를 제공하기 좋은 사례예요.
export type ArrayMetadata<ArrType> = {
length: number;
firstObject: ArrType | undefined;
};
export function getArrayMetadata<ArrType>(
arr: ArrType[]
): ArrayMetadata<ArrType>;
이제 배열의 타입이 ArrayMetadata 타입 안으로 그대로 전파됩니다.
내보낸 타입은 모듈의 소비자가 TypeScript 코드에서 import나 import type으로 재사용하거나, JSDoc imports로 가져다 쓸 수 있어요.
모듈 코드 안의 Namespace
자바스크립트 코드의 런타임 관계를 타입으로 표현하는 일은 꽤 까다롭습니다. ES Module 같은 문법으로 내보내기를 설명하기에 부족할 때 namespaces를 쓰면 돼요.
예를 들어, 타입이 복잡해서 .d.ts 안에서 namespace로 묶어두고 싶은 경우입니다.
// This represents the JavaScript class which would be available at runtime
export class API {
constructor(baseURL: string);
getInfo(opts: API.InfoRequest): API.InfoResponse;
}
// This namespace is merged with the API class and allows for consumers, and this file
// to have types which are nested away in their own sections.
declare namespace API {
export interface InfoRequest {
id: string;
}
export interface InfoResponse {
width: number;
height: number;
}
}
.d.ts 파일에서 namespace가 어떻게 동작하는지 이해하려면 .d.ts 심층 분석 문서를 읽어보세요.
선택적인 전역 사용 (Optional Global Usage)
export as namespace를 쓰면 UMD 환경에서 모듈이 전역 스코프에서도 사용 가능하다고 선언할 수 있어요.
export as namespace moduleName;
참고 예시 (Reference Example)
이 모든 조각이 어떻게 하나로 모이는지 감을 잡기 위해, 새 모듈을 만들 때 출발점으로 삼을 만한 참고 .d.ts를 보여드릴게요.
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]
// Project: [~THE PROJECT NAME~]
// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>
/*~ This is the module template file. You should rename it to index.d.ts
*~ and place it in a folder with the same name as the module.
*~ For example, if you were writing a file for "super-greeter", this
*~ file should be 'super-greeter/index.d.ts'
*/
/*~ If this module is a UMD module that exposes a global variable 'myLib' when
*~ loaded outside a module loader environment, declare that global here.
*~ Otherwise, delete this declaration.
*/
export as namespace myLib;
/*~ If this module exports functions, declare them like so.
*/
export function myFunction(a: string): string;
export function myOtherFunction(a: number): number;
/*~ You can declare types that are available via importing the module */
export interface SomeType {
name: string;
length: number;
extras?: string[];
}
/*~ You can declare properties of the module using const, let, or var */
export const myField: number;
라이브러리 파일 배치
선언 파일의 배치(layout)는 라이브러리의 배치를 그대로 따라야 합니다. 라이브러리는 여러 모듈로 구성될 수 있어요.
myLib
+---- index.js
+---- foo.js
+---- bar
+---- index.js
+---- baz.js
이들은 이렇게 import 될 수 있어요.
var a = require("myLib");
var b = require("myLib/foo");
var c = require("myLib/bar");
var d = require("myLib/bar/baz");
그렇다면 선언 파일도 이렇게 되어야 합니다.
@types/myLib
+---- index.d.ts
+---- foo.d.ts
+---- bar
+---- index.d.ts
+---- baz.d.ts
타입 테스트하기 (Testing your types)
이 변경을 DefinitelyTyped에 제출해서 모두가 쓰게 하려면, 다음을 권장합니다.
node_modules/@types/[libname]안에 새 폴더를 만든다- 그 폴더에
index.d.ts를 만들고 예시를 복사한다- 모듈을 쓰는 코드가 어디서 깨지는지 확인하며 index.d.ts를 채워나간다
- 만족스러우면 DefinitelyTyped/DefinitelyTyped를 클론하고 README의 지침을 따른다
그게 아니라면:
- 소스 트리 루트에 새 파일을 만든다:
[libname].d.tsdeclare module "[libname]" { }를 추가한다- declare module의 중괄호 안에 템플릿을 넣고, 사용하는 코드가 어디서 깨지는지 확인한다