← Курс/Declaration Files (.d.ts)#184 из 257+25 XP

Declaration Files — типы без реализации

Что такое .d.ts файлы

Declaration files содержат **только типы** — без исполняемого кода. Они говорят TypeScript «это JavaScript-модуль с такими-то типами»:

// utils.d.ts — описание типов для utils.js
declare function formatDate(date: Date, format: string): string
declare function parseDate(str: string): Date | null

declare const DEFAULT_FORMAT: string

declare interface DateOptions {
  locale?: string
  timezone?: string
}

Ключевое слово `declare`

declare говорит TS: «эта вещь существует, но её реализация где-то ещё»:

// Глобальная переменная (например, injected build tool)
declare const __DEV__: boolean
declare const __VERSION__: string

// Глобальная функция (из CDN-скрипта)
declare function analytics(event: string, data?: object): void

// Класс без реализации
declare class EventEmitter {
  on(event: string, listener: Function): this
  off(event: string, listener: Function): this
  emit(event: string, ...args: any[]): boolean
}

Module Augmentation — расширение чужих типов

Позволяет добавлять типы к существующим модулям без изменения их исходников:

// Расширяем типы express Request
import 'express'

declare module 'express' {
  interface Request {
    user?: { id: number; role: string }
    requestId: string
  }
}

// Теперь в обработчиках req.user и req.requestId — типизированы
app.get('/profile', (req, res) => {
  console.log(req.user?.id)  // TypeScript знает тип
})

declare global — расширение глобального пространства

// Добавляем типы к window в браузере
declare global {
  interface Window {
    analytics: (event: string) => void
    __APP_CONFIG__: { apiUrl: string; version: string }
  }

  // Расширяем Array прототип
  interface Array<T> {
    last(): T | undefined
  }
}

// Теперь window.__APP_CONFIG__ типизирован

DefinitelyTyped и @types

Для популярных JS библиотек типы хранятся в DefinitelyTyped:

npm install lodash           # сама библиотека (JS)
npm install @types/lodash    # типы для неё (.d.ts файлы)

Современные библиотеки часто включают типы сами:

// axios, zod, prisma — типы встроены, @types не нужен
import axios from 'axios'    // TypeScript сразу знает все типы

/// reference и typeRoots

/// <reference types="node" />
// Говорит TS включить типы из @types/node

// В tsconfig.json:
// "typeRoots": ["./node_modules/@types", "./custom-types"]
// "types": ["node", "jest"]  — только эти @types

Написание типов для JS библиотеки

// Типы для гипотетической библиотеки my-lib
declare module 'my-lib' {
  export function createClient(options: ClientOptions): Client

  export interface ClientOptions {
    baseUrl: string
    timeout?: number
    headers?: Record<string, string>
  }

  export interface Client {
    get<T>(path: string): Promise<T>
    post<T>(path: string, data: unknown): Promise<T>
  }

  export default createClient
}

Примеры

Симуляция module augmentation в JS: base объект с расширениями через Object.assign и метод createPlugin для declaration merging

// В TypeScript declaration merging позволяет расширять
// существующие модули и интерфейсы без изменения исходников.
// В JavaScript симулируем это через Object.assign и паттерн плагинов.

// Базовый объект utils (имитирует JS-библиотеку без типов)
const utils = {
  version: '1.0.0',

  log(message) {
    console.log(`[utils] ${message}`)
  },

  formatDate(date) {
    return date.toISOString().split('T')[0]
  }
}

// "Module augmentation" — расширяем utils новыми методами
// (аналог declare module 'utils' { ... } в TypeScript)
Object.assign(utils, {
  logWithTimestamp(message) {
    const time = new Date().toLocaleTimeString()
    console.log(`[utils ${time}] ${message}`)
  },

  formatCurrency(amount, currency = 'RUB') {
    return `${amount.toFixed(2)} ${currency}`
  }
})

// createPlugin — объединяет базовый объект с расширением
// Если метод с таким именем уже есть — вызывает оба
function createPlugin(base, extension) {
  const plugin = Object.assign({}, base)

  Object.keys(extension).forEach(key => {
    if (typeof extension[key] === 'function' && typeof base[key] === 'function') {
      // Оба метода существуют — создаём composed функцию
      const originalFn = base[key]
      const extensionFn = extension[key]
      plugin[key] = function(...args) {
        const result = originalFn.apply(this, args)
        extensionFn.apply(this, args)
        return result
      }
    } else {
      plugin[key] = extension[key]
    }
  })

  return plugin
}

// Array.prototype augmentation (добавляем метод last)
// Аналог declare global { interface Array<T> { last(): T } }
Array.prototype.last = function() {
  return this[this.length - 1]
}

// --- Демонстрация ---

console.log('=== utils (после augmentation) ===')
utils.log('базовый метод работает')
utils.logWithTimestamp('расширенный метод')
console.log(utils.formatCurrency(1234.5))  // '1234.50 RUB'
console.log(utils.formatDate(new Date('2024-01-15')))  // '2024-01-15'

console.log('\n=== createPlugin ===')
const logger = {
  log(msg) { console.log(`LOG: ${msg}`) },
  prefix: 'app'
}

const formattingExtension = {
  log(msg) { console.log(`(extension also received: ${msg})`) },
  format(msg) { return `[${this.prefix.toUpperCase()}] ${msg}` }
}

const enhancedLogger = createPlugin(logger, formattingExtension)
enhancedLogger.log('hello')   // LOG: hello + (extension also received: hello)
console.log(enhancedLogger.format('test'))  // '[APP] test'

console.log('\n=== Array.prototype.last ===')
const nums = [1, 2, 3, 4, 5]
console.log(nums.last())   // 5
console.log([].last())     // undefined