모듈 .d.ts 템플릿
모듈 .d.ts 템플릿
JavaScript 라이브러리 하나를 타입스크립트에서 쓰고 싶을 때, 그 라이브러리의 타입을 어떻게 선언해야 할지 막막해지기 마련이에요. 이 페이지는 그런 모듈용 .d.ts 파일을 처음부터 어떻게 짜는지, 실제 템플릿을 따라가며 하나씩 알려드릴게요.
출처: TypeScript 공식문서
본문
Common 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;
참고로 타입스크립트 플레이그라운드는 JavaScript 코드를 .d.ts로 바꿔서 어떤 모습인지 보여줘요. 궁금하면 직접 해 보세요.
.d.ts 문법은 일부러 ES Modules 문법과 비슷하게 생겼어요. ES Modules는 2015년에 ES2015(ES6)의 일부로 TC39에서 승인됐는데, 그보다 훨씬 전부터 트랜스파일러로는 쓸 수 있었거든요. 그래서 만약 JavaScript 코드베이스가 ES Modules를 쓰고 있다면:
export function getArrayLength(arr) {
return arr.length;
}
이건 다음과 같은 .d.ts와 같아요.
export function getArrayLength(arr: any[]): number;
기본 내보내기 (Default Exports)
CommonJS에서는 어떤 값이든 기본 내보내기(default export)로 내보낼 수 있어요. 예를 들어 정규식 모듈이 하나 있다고 해 볼게요.
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 참조 페이지를 함께 봐 주세요.
가져오는 쪽이 많을 때 다루기 (Handling Many Consuming 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";
이 모든 경우를 다 지원하려면 JavaScript 코드 쪽에서 실제로 이 패턴들을 전부 지원해야 해요. 이런 여러 패턴을 지원하려면 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;
모듈 안의 타입 (Types in Modules)
아직 존재하지 않는 JavaScript 코드에 타입을 만들어 주고 싶을 때가 있을 거예요.
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 타입 안으로 그대로 전파돼요.
내보낸 타입들은 곧 모듈을 쓰는 쪽에서 타입스크립트 코드의 import나 import type, 또는 JSDoc imports로 다시 재사용할 수 있어요.
모듈 코드 안의 네임스페이스 (Namespaces in Module Code)
JavaScript 코드의 런타임 관계를 타입으로 설명하려다 보면 까다로울 때가 있어요. ES Module 비슷한 문법만으로는 내보내기를 설명하기 부족할 때, 그때 네임스페이스를 쓸 수 있어요.
예를 들어 타입이 충분히 복잡해서 .d.ts 안에 네임스페이스로 묶어 두고 싶은 상황이 있다고 해 볼게요.
// 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 파일에서 네임스페이스가 어떻게 동작하는지 자세히 알고 싶다면 .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;
라이브러리 파일 배치 (Library file layout)
선언 파일의 배치는 라이브러리의 배치를 그대로 따라가야 해요. 라이브러리는 여러 모듈로 이루어질 수 있어요.
myLib
+---- index.js
+---- foo.js
+---- bar
+---- index.js
+---- baz.js
이런 것들은 이렇게 가져올 수 있어요.
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.ts declare module "[libname]" { }를 추가한다.declare module의 중괄호 안에 템플릿을 넣고, 사용이 어디서 깨지는지 확인한다.