Skip to Content
WebTypeScriptTypeScript13. 선언 파일과 서드파티 타입

이번 편의 결과물: 시드 거래 데이터를 타입과 함께 JSON에서 불러오고, 타입을 전혀 제공하지 않는 날짜 포맷 라이브러리에 직접 선언 파일을 작성해 붙입니다. · 다루는 개념: .d.ts 작성, JSON 모듈 타입 선언, @types 서드파티 타입 소비, 앰비언트 선언(declare)

12편까지 expense-trackermodels/, services/, ui/로 모듈이 나뉘었고, 거래는 화면 폼으로 직접 입력해야만 생겼습니다. 이 편에서는 앱을 처음 열었을 때 보여줄 시드 데이터를 JSON 파일로 준비하고, id를 만들어 온전한 거래를 조립하는 로직을 services/createTransaction.ts로 뽑아냅니다. 이어서 타입이 없는 서드파티 코드를 TypeScript와 함께 쓰는 방법을 다룹니다.

이 편에서 만드는 파일

expense-tracker/ ├── package.json ~ (date-format-lib 추가) └── src/ ├── data/ │ └── seedTransactions.json + (시드 거래 5건) ├── services/ │ └── createTransaction.ts + (draft에 crypto.randomUUID로 id를 채워 거래를 만드는 함수) ├── types/ │ └── date-format-lib.d.ts + (date-format-lib 패키지용 앰비언트 선언) └── main.ts ~ (시드 데이터 로드, createTransaction 사용, 오늘 날짜 표시)

개념 정리

타입이 없는 데이터·코드를 TypeScript 프로젝트에 들여오는 경로는 상황마다 다릅니다.

상황타입은 어디서 오는가
JSON 파일 importresolveJsonModule 옵션이 켜져 있으면 파일 내용으로부터 구조적으로 추론
자체 타입도 @types 패키지도 없는 npm 패키지직접 작성한 .d.ts에서 declare module로 형태를 선언
DefinitelyTyped에 타입이 등록된 npm 패키지@types/패키지이름을 devDependencies로 설치
실행 코드 없이 타입만 알리고 싶을 때declare로 앰비언트 선언

Vite vanilla-ts 기본 tsconfig.json에는 resolveJsonModule: true가 들어 있어 별도 설정 없이 JSON을 import할 수 있습니다. 다만 추론된 타입은 파일에 적힌 값 그대로 string, number 같은 일반 타입이지, ExpenseCategory'income'/'expense' 같은 좁은 리터럴 유니온이 아닙니다. 도메인 타입으로 쓰려면 타입 단언(as)으로 좁혀야 하고, 이 단언은 컴파일러만 설득할 뿐 실제 값이 맞는지는 검사하지 않습니다.

이번 편에서 쓸 date-format-lib는 자체 타입도 없고 DefinitelyTyped에 @types/date-format-lib도 등록되어 있지 않다고 가정합니다. 이런 패키지를 쓰려면 프로젝트 안에 직접 .d.ts를 작성해 형태를 알려줘야 합니다. 반대로 어떤 패키지가 @types/패키지이름으로 타입을 제공한다면, 그 패키지를 devDependencies로 설치하는 것만으로 별도 .d.ts 작성 없이 타입 지원을 받을 수 있습니다. .d.ts 안에서 declare module '패키지이름'처럼 쓰는 문장은 실행 코드 없이 이름과 타입만 컴파일러에 알리는 앰비언트 선언입니다.

실습

1. 시드 데이터를 JSON으로 준비

expense-tracker/src/data/seedTransactions.json 파일을 새로 만듭니다.

[ { "id": "seed-1", "type": "income", "date": "2026-08-01", "category": "salary", "amount": 3200000, "memo": "8월 급여" }, { "id": "seed-2", "type": "expense", "date": "2026-08-03", "category": "housing", "amount": 850000, "memo": "월세" }, { "id": "seed-3", "type": "expense", "date": "2026-08-05", "category": "food", "amount": 42000, "memo": "장보기" }, { "id": "seed-4", "type": "expense", "date": "2026-08-10", "category": "transport", "amount": 15000, "memo": "지하철 충전" }, { "id": "seed-5", "type": "income", "date": "2026-08-15", "category": "bonus", "amount": 500000, "memo": "성과급" } ]

이 파일을 import하면 각 필드는 string 또는 number로만 추론됩니다. categoryExpenseCategory·IncomeCategory 유니온에 속하는지, type'income'/'expense' 둘 중 하나인지는 TypeScript가 확인해주지 않습니다.

2. id 생성 로직을 함수로 분리

12편의 main.ts는 폼에서 만든 draft{ ...draft, id: crypto.randomUUID() } as Transaction을 그 자리에서 바로 적용했습니다. 이 로직을 재사용할 수 있도록 함수로 뽑아냅니다.

// expense-tracker/src/services/createTransaction.ts import type { Transaction } from '../models' import type { NewTransactionDraft } from '../ui/transactionForm.ts' export function createTransaction(draft: NewTransactionDraft): Transaction { return { ...draft, id: crypto.randomUUID() } as Transaction }

NewTransactionDraft는 12편에서 ui/transactionForm.ts에 이미 Omit<Transaction, 'id'>(10편)로 정의해 둔 타입을 그대로 가져다 씁니다. crypto.randomUUID()는 브라우저와 Node.js 모두에 내장된 함수로, 호출할 때마다 고유한 문자열 id를 만듭니다.

3. 타입 없는 날짜 포맷 라이브러리에 앰비언트 선언 작성

npm install date-format-lib
// expense-tracker/package.json에 추가되는 부분 { "dependencies": { "date-format-lib": "^1.2.0" } }

date-format-lib는 실행 코드만 배포하고 타입 선언은 전혀 없습니다. import { formatDate } from 'date-format-lib'라고만 쓰면 “Could not find a declaration file” 오류가 납니다. 프로젝트 안에 직접 선언 파일을 작성합니다.

// expense-tracker/src/types/date-format-lib.d.ts declare module 'date-format-lib' { export function formatDate(date: Date, pattern: string): string }

이 파일은 실제 구현을 담지 않습니다. date-format-lib라는 모듈 이름으로 import했을 때 formatDate 함수가 이런 시그니처로 존재한다고 컴파일러에 알리기만 합니다.

4. main.ts에 연결

// expense-tracker/src/main.ts (발췌 — 이 편에서 추가·수정된 부분만) import type { Transaction } from './models' import { Storage } from './services' import { createTransaction } from './services/createTransaction.ts' import seedRaw from './data/seedTransactions.json' import { formatDate } from 'date-format-lib' const storage = new Storage<Transaction>('expense-tracker:transactions') if (storage.load().length === 0) { const seedTransactions = seedRaw as Transaction[] storage.save(seedTransactions) } let transactions: Transaction[] = storage.load() // app.innerHTML 템플릿에 <p id="today-label"></p>를 한 줄 추가한 뒤 const todayLabel = app.querySelector<HTMLParagraphElement>('#today-label')! todayLabel.textContent = formatDate(new Date(), 'yyyy년 mm월 dd일') // onCreate 핸들러 안에서 id 조립을 createTransaction으로 교체 const form = mountTransactionForm(formRoot, { onCreate: (draft) => { const transaction = createTransaction(draft) transactions.push(transaction) storage.save(transactions) renderAll() }, // onUpdate는 그대로 유지 })

seedRaw as Transaction[]는 JSON에서 추론된 느슨한 타입을 도메인 타입으로 좁히는 단언입니다. formatDatedate-format-lib.d.ts 덕분에 인자·반환값 타입 검사와 자동완성을 모두 받습니다.

5. 실행과 확인

npm run dev
  • localStorage를 비운 뒤 처음 여는 브라우저에서 시드 거래 5건이 목록에 나타난다.
  • 화면에 “2026년 08월 20일”처럼 오늘 날짜가 상단에 표시된다.
  • 폼으로 거래를 새로 추가해도 이전과 동일하게 동작한다.
  • npx tsc --noEmit을 실행해도 타입 오류가 없다.

직접 해보기

  1. seedTransactions.jsoncategory 값 하나를 "fod"(오타)로 바꿔보세요. npx tsc --noEmit이 여전히 통과하는지 확인하고, 왜 컴파일러가 이 오류를 못 잡는지 생각해보세요.
  2. date-format-lib.d.ts를 잠시 지우고 npm run dev를 실행해보세요. 편집기와 터미널에 각각 어떤 메시지가 나오는지 확인한 뒤 파일을 되돌립니다.

정답 보기

1번은 npx tsc --noEmit이 그대로 통과합니다. as Transaction[] 단언은 컴파일 타임에만 적용되고 실제 JSON 값을 검사하지 않기 때문입니다. 오타는 화면에 이상한 카테고리가 보일 때에야 드러납니다.

2번은 import { formatDate } from 'date-format-lib' 위치에서 타입 선언을 찾을 수 없다는 오류가 표시됩니다. date-format-lib 패키지 자체는 여전히 node_modules에 있으므로 번들링·실행에는 문제가 없지만, 타입 검사 단계에서만 오류가 납니다.

자주 하는 실수

증상원인고치는 법
JSON에서 가져온 값의 필드가 잘못되어도 에러가 안 남as Transaction[] 단언은 컴파일 타임에만 작동, 실제 값은 검사하지 않음시드 데이터를 수정할 때마다 값을 직접 확인한다
date-format-lib 모듈의 타입 선언을 찾지 못한다는 에러.d.ts 파일이 프로젝트에 포함되지 않았거나 declare module의 패키지 이름 문자열이 실제 패키지 이름과 다름types/date-format-lib.d.ts의 문자열이 package.json의 패키지 이름과 정확히 같은지 확인한다
createTransaction에서 프로퍼티가 없다는 오류폼에서 만든 draft가 NewTransactionDraft가 요구하는 프로퍼티를 빠뜨림ui/transactionForm.tsNewTransactionDraft 정의와 정확히 같은 모양인지 확인한다
.d.ts에 함수 본문을 작성해 에러 발생.d.ts는 타입 선언 전용이라 구현을 가질 수 없음시그니처만 남기고 실행 코드는 패키지 본체에 맡긴다

확인 문제

문제 14지선다
resolveJsonModule이 켜진 상태에서 JSON 파일을 import했을 때 category 필드가 좁은 유니온이 아니라 일반 문자열로 추론되는 이유는
문제 24지선다
date-format-lib처럼 자체 타입도 types 패키지도 없는 npm 패키지를 쓰는 방법은
문제 34지선다
어떤 npm 패키지가 DefinitelyTyped에 타입을 등록해 두었다면 타입 지원을 받는 가장 간단한 방법은
문제 44지선다
d.ts 파일 안에서 declare module처럼 구현 없이 형태만 알리는 선언을 부르는 말은

참고 자료

Last updated on