안동민 개발노트

본문 시작

타입 선언 파일 작성 및 사용

타입이 없는 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' 형식의 매개 변수에 할당될 수 없습니다.
컴파일러 동작

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

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

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

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

소비 import는 구현과 선언을 서로 다른 경로에서 찾는다

SIDECAR · GLOBAL · PACKAGE

소비 import는 구현과 선언을 서로 다른 경로에서 찾는다

local sidecar, 프로젝트에 포함된 global 선언, package types/exports는 각기 다른 resolution 경계를 가진다.

소비 import는 구현과 선언을 서로 다른 경로에서 찾는다 local sidecar, 프로젝트에 포함된 global 선언, package types/exports는 각기 다른 resolution 경계를 가진다. 소비 import정확한 specifierlocal sidecarsame basename .d.tsresolution구현 + 선언 결합package boundaryexports · typesglobal includefiles · include · types
  1. local sidecar는 JavaScript와

    local sidecar는 JavaScript와 같은 basename의 top-level export를 쓴다.

  2. global 선언은 프로젝트

    global 선언은 프로젝트 files/include에 실제로 포함돼야 한다.

  3. package root와 subpath의

    package root와 subpath의 JavaScript·types 조건을 함께 연결한다.

  4. 선언 경로가 맞아도

    선언 경로가 맞아도 런타임 구현 파일은 별도로 존재해야 한다.

상대 경로 ambient module은 local sidecar의 기본 패턴이 아니다.


전역 타입 선언 (.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 디렉터리를 지정합니다.

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


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

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

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

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

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

선언은 런타임 API와 함께 검증하고 갱신한다

OBSERVE · DECLARE · CONSUME · PUBLISH

선언은 런타임 API와 함께 검증하고 갱신한다

실제 API 관찰에서 시작해 positive/negative 소비 검사, declaration emit, package 연결, 변경 동기화로 이어진다.

선언은 런타임 API와 함께 검증하고 갱신한다 실제 API 관찰에서 시작해 positive/negative 소비 검사, declaration emit, package 연결, 변경 동기화로 이어진다. STEP 1API 관찰실제 export shapeSTEP 2.d.ts 작성정확한 specifierSTEP 3소비 검사positive · negativeSTEP 4배포 연결declaration · exportsSTEP 5변경 동기화runtime + typesOBSERVE · DECLARE · CONSUME · PUBLISH
  1. 런타임 API와 export

    런타임 API와 export 모양을 먼저 관찰한다.

  2. 정확한 import specifier와

    정확한 import specifier와 호출 표면을 선언한다.

  3. positive 실행과 negative

    positive 실행과 negative 타입 소비를 함께 검사한다.

  4. declaration emit과 package

    declaration emit과 package exports/types 파일을 배포한다.

  5. API 변경 때

    API 변경 때 구현·선언·consumer를 함께 갱신한다.

손으로 쓴 선언은 JavaScript 구현과 자동으로 의미 비교되지 않는다.

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


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