Menu

Declare в TypeScript: файлы объявлений .d.ts

Файл .d.ts описывает типы JavaScript-кода, не содержа самого кода, а ключевое слово declare делает то же внутри файла .ts. Как генерируются файлы объявлений, где место пакетам @types, как типизировать модуль без типов и как declare global и расширение модулей дополняют существующие типы.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Файл объявлений (.d.ts) содержит типы и больше ничего: сигнатуры, интерфейсы и формы классов для JavaScript, который находится где-то ещё. Ключевое слово declare делает ту же работу внутри обычного файла. Оно говорит компилятору «это существует во время выполнения, поверь мне» и не генерирует никакого кода.

Компилятор принял __APP_VERSION__.length, потому что объявление говорит, что это строка. Во время выполнения такой переменной нет, поэтому чтение .length бросает ReferenceError: __APP_VERSION__ is not defined, исключение, которого компилятор не мог предвидеть. Это контракт любого объявления: типы верны ровно настолько, насколько верен стоящий за ними JavaScript.

Что содержит файл .d.ts

Лучше всего идею показывает компиляция с declaration: true. Из такого исходника:

// price.ts
export interface LineItem {
    name: string;
    price: number;
    qty: number;
}

const TAX = 0.2;

export function total(items: LineItem[]) {
    const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    return Math.round(sum * (1 + TAX) * 100) / 100;
}

export class Cart {
    private items: LineItem[] = [];
    add(item: LineItem) {
        this.items.push(item);
        return this;
    }
}

tsc создаёт price.js и такой price.d.ts:

export interface LineItem {
    name: string;
    price: number;
    qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
    private items;
    add(item: LineItem): this;
}

Тела функций исчезли, выведенные возвращаемые типы записаны явно (number, this), приватное поле сохранило имя, но потеряло тип, а неэкспортированного TAX нет вовсе. Библиотека публикует .js для Node и .d.ts для вашего редактора и компилятора. emitDeclarationOnly: true создаёт только файлы .d.ts для проектов, где JavaScript собирает бандлер.

Встроенные объекты ES2022, которыми вы пользуетесь каждый день, тоже берутся из файлов объявлений: lib.es2022.d.ts и соседние файлы поставляются с TypeScript и выбираются через target и lib.

Формы declare

Каждая инструкция declare описывает то, что уже существует. В файле .d.ts без import и export (глобальном файле объявлений) каждая из них становится видна всему проекту:

// globals.d.ts
declare const API_URL: string;                     // a global constant
declare let debugMode: boolean;                    // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget {                             // a class from a script tag
    constructor(el: string);
    render(): void;
}
declare namespace Analytics {                      // a global object
    function page(name: string): void;
}
declare module "legacy-charts" {                   // a module you import
    export function draw(data: number[]): void;
}

Внутри файла .d.ts для interface и type слово declare не нужно; любое другое объявление верхнего уровня без declare или export даёт ошибку TS1046. Как только в файле появляется import или export, его объявления становятся локальными, а дополнения глобальной области видимости пишутся в блоке declare global (показан ниже). Именно по объявленной сигнатуре компилятор проверяет вызовы:

Компилятор сообщает index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. Передайте { items: "3" } или измените объявление, если настоящая функция принимает числа.

Пакеты @types

Многие пакеты npm поставляются со своими файлами .d.ts, на которые ссылается поле types (или условие types в exports) их package.json. Для пакетов только на JavaScript проект DefinitelyTyped, поддерживаемый сообществом, публикует типы в пространстве @types:

npm i lodash
npm i -D @types/lodash

Когда вы импортируете пакет через import, TypeScript автоматически ищет node_modules/@types/{name}. Типы, которые описывают глобальные объекты, а не импорт, например @types/node (process, Buffer) или describe и it тестового фреймворка, нужно перечислить в tsconfig.json:

{
    "compilerOptions": {
        "types": ["node"]
    }
}

Без этой записи TypeScript 7 их не загружает, даже если пакет установлен:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

tsc --init записывает в новый конфиг "types": [] с комментарием, предлагающим ["node"] для проектов на Node.

Типизация модуля без типов

Импорт пакета на JavaScript без типов, ни встроенных, ни в @types, даёт ошибку TS7016:

error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.

Исправьте её файлом .d.ts в любом месте проекта (обычно в папке types/; главное, чтобы она попадала под include) без import и export на верхнем уровне. Опишите то, чем вы пользуетесь:

// types/fakelib.d.ts
declare module "fakelib" {
    export function hi(name: string): string;
    export const version: string;
}

// Non-code files a bundler lets you import
declare module "*.svg" {
    const url: string;
    export default url;
}

Самый короткий вариант, одна строка declare module "fakelib";, делает любой импорт из пакета any. Он убирает ошибку и вместе с ней все проверки, поэтому считайте его временной мерой.

declare global

Коду, который дополняет глобальную область видимости, например полифилу, новому методу встроенного объекта или глобальной переменной из тега script, нужны соответствующие типы. Их добавляет declare global. Он должен находиться в модуле (файле с import или export), поэтому наверху стоит export {}:

interface Array<T> сливается со встроенным интерфейсом Array, а не заменяет его, а var (не let и не const) добавляет свойство в globalThis. Расширять встроенные прототипы в общем коде рискованно; тем же приёмом с declare global проекты добавляют свои переменные в process.env через интерфейс NodeJS.ProcessEnv из @types/node.

Расширение модулей

Чтобы дополнить типы импортируемого пакета, объявите заново имя его модуля и снова откройте интерфейс. Сам файл должен быть модулем (это обеспечивает import; подойдёт и export {}). Без import или export тот же блок объявит совершенно новый модуль config-lib, который скроет настоящие типы пакета:

// types/config-lib.d.ts
import "config-lib";

declare module "config-lib" {
    interface Settings {
        beta: boolean; // merged into the package's own Settings interface
    }
}

После этого load().beta из config-lib везде имеет тип boolean. Интерфейсы сливаются, а псевдонимы типов нет, поэтому для такого приёма библиотека должна экспортировать interface. Так плагины добавляют поля в объекты запроса или конфигурации фреймворка.

skipLibCheck

skipLibCheck: true отключает проверку типов в файлах .d.ts, включая файлы в node_modules. Ваш собственный код по-прежнему проверяется на соответствие им. Это экономит время и избавляет от ошибок из-за двух пакетов с несогласованными объявлениями, поэтому tsc --init включает эту опцию. Цена в том, что ошибка внутри ваших собственных файлов .d.ts тоже не будет обнаружена.

Часто задаваемые вопросы

Что такое файл .d.ts в TypeScript?

Файл объявлений: в нём только типы (сигнатуры функций, интерфейсы, формы классов) и никакой реализации. Он описывает JavaScript, который существует где-то ещё, например скомпилированную библиотеку или встроенные API браузера, чтобы TypeScript мог проверять использующий его код. tsc никогда не генерирует для него JavaScript.

Что делает ключевое слово declare в TypeScript?

Сообщает компилятору, что значение существует во время выполнения, не создавая его. declare const VERSION: string; позволяет использовать VERSION как строку, а сама строка исчезает из результата. Если на самом деле VERSION никто не определяет, программа падает во время выполнения с ReferenceError.

Как исправить «Could not find a declaration file for module»?

Ошибка TS7016 означает, что у пакета нет типов. Установите их, если они существуют (npm i -D @types/package-name), или добавьте файл .d.ts с declare module "package-name" { ... }, описывающий то, что вы используете. Одна строка declare module "package-name"; убирает ошибку, типизируя весь модуль как any.

Как сгенерировать файлы .d.ts из TypeScript?

Задайте "declaration": true в tsconfig.json (или передайте --declaration). Тогда каждый файл .ts создаёт .d.ts рядом со своим .js. Добавьте emitDeclarationOnly, если JavaScript собирает другой инструмент, и declarationMap, чтобы редакторы могли переходить от типов к вашему исходному коду.

Чем declare global отличается от declare module?

declare global { ... } дополняет глобальную область видимости, например новым свойством у Array или глобальной переменной. declare module "name" { ... } описывает модуль, который вы импортируете под этим именем. declare global должен находиться в модуле (файле с import или export); declare module в файле без импортов и экспортов объявляет новый модуль, а внутри модуля расширяет существующий.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ