*.d.ts

Что такое *.d.ts файлы и когда их нужно писать

*.d.ts файлы - это файлы декларации типов. Они содержат описание типов без реализации. Например, сигнатуры функций без тела, название и тип глобальных переменных без значений. В основном файлы декларации типов нужны для написания библиотек. Они нужны тогда, когда:

  • вы написали библиотеку и хотите, чтобы ее можно было использовать не только в TypeScript проектах

  • вы хотите использовать библиотеку, которая написана на JavaScript.

Рассмотрим ситуацию. Вы написали библиотеку для работы с математическим представлением вектора.

/* .ts */
class Vector {
  static readonly zero = new Vector(0, 0);

  constructor(public readonly x: number, public readonly y: number) {
  }

  public add(vector: Vector): Vector {
    return new Vector(this.x + vector.x, this.y + vector.y);
  }
}

Если выложить этот код в качестве библиотеки (например, сделать и опубликовать npm-пакет), то этим кодом смогут пользоваться только проекты с настроенным TypeScript. Более того, возможно потребуется дополнительная настройка TypeScript, чтобы при компиляции проекта он компилировал и файлы нашей библиотеки.

Другой способ публикации библиотеки - скомпилировать TypeScript в JavaScript и выложить уже JavaScript файлы. В таком случаи этой библиотекой могут пользоваться любые проекты, как обычной библиотекой на JavaScript. Однако, если эту библиотеку планируется использовать в TypeScript проекте - хотелось бы сохранить типизацию. Здесь и появляются .d.ts файлы. Перед публикацией библиотеки мы компилируем TypeScript в JavaScript, но конфигурируем компилятор так, чтобы рядом с получившимися .js файлами, он создал .d.ts файлы по каждому файлу JavaScript-а. Далее мы публикуем и .js файлы и .d.ts файлы. Теперь, если использовать эту библиотеку в JavaScript проекте, то будут просто использоваться .js файлы, если использовать в TypeScript проекте, то при компиляции будут использоваться .d.ts, а runtime пред компилированные .js.

Синтаксис декларации типов

*.d.ts файлы можно получить автоматически или написать вручную. Для автоматической генерации нужно, чтобы библиотека изначально был на TypeScript и компилятор был настроен на генерацию файлов декларации. Например, для кода выше компилятор сгенерирует такие декларации:

/* .d.ts */
declare class Vector {
    readonly x: number;
    readonly y: number;
    static readonly zero: Vector;
    constructor(x: number, y: number);
    add(vector: Vector): Vector;
}

Если библиотека изначально написана на JavaScript (или на другом языке, но не на TypeScript) или автоматическая генерация деклараций типов по TypeScript-файлам по каким-то причинам не устраивает, то файлы деклараций можно написать вручную. Возможно есть сторонние генераторы *.d.ts файлов по JavaScript, TypeScript и другим языкам, но их мы рассматривать не будем.

Одно из главных ключевых слов в файлах деклараций - это слово declare. Используя это слово в какой-либо синтаксической конструкции (например, в объявлении класса), мы указываем TypeScript-у, что ее нужно интерпретировать, как декларацию. Рассмотрим два блока кода выше. При описании класса в *.ts файле мы описывали всю сигнатуру класса, правила инициализации полей, тела конструктора и методов и модификаторы доступа. В файле декларации класс помечен словом declare и у него описан лишь публичный интерфейс взаимодействия с этим классом без реализации - что и с какими параметрами можно вызывать, что в итоге получится, а также к каким полям и методам есть доступ.

Рассмотрим автоматическую генерацию файлов декларации и то, как ее можно изменять. Если объявлены глобальные переменные

/* .ts */
const globalVariable1: string = "lalaka";
let globalVariable2: number = 48;
var globalVariable3: 15 = 15;

то в их декларациях используется declare и сохраняется способ объявления, но теряется значение - оно с точки зрения проверки на типы не важно:

/* .d.ts */
declare const globalVariable1: string;
declare let globalVariable2: number;
declare var globalVariable3: 15;

Однако, если явно не указывать тип переменных, то файл декларации немного изменится:

/* .ts */
const globalVariable1 = "lalaka";
let globalVariable2 = 48;
var globalVariable3 = 15;

/* .d.ts */
declare const globalVariable1 = "lalaka";
declare let globalVariable2: number;
declare var globalVariable3: number;

Для переменной объявленной с использованием const автоматический вывод типов может вывести литеральный тип.

Для декларации функций нужно указать их сигнатуры - название, параметры и их типы и возвращаемое значение.

/* .ts */
function foo(a: number, b: string): boolean {
    return a.toString() === b;
}

/* .d.ts */
declare function foo(a: number, b: string): boolean;

Если функция использует generic-и, то в декларации нужно указать и generic-параметры и их ограничения.

/* .ts */
function foo<T extends number, R>(a: T): R {
    ...
}

/* .d.ts */
declare function foo<T extends number, R>(a: T): R;

Для объявления интерфейсов и type alias-ов ключевое слово declare можно опускать.

/* .d.ts */
interface A {
    foo: () => void;
}

type B = {
    bar: number
}

Для декларации классов нужно использовать особенности декларации переменных и функций. По их подобию сделана декларация полей, методов и конструктора. Ключевое слово declare используется только у самого класса. Поля декларируются со всеми модификаторами, но без инициализации. Методы также декларируются со всеми модификаторами и описанной сигнатурой, но без тела.

/* .d.ts */
declare class Foo {
    public a: number;
    private b: string;
    private e: boolean;
    constructor(q: [number, string, boolean]);
    getAll(): [number, string, boolean];
    static create(a: number): Foo;
}

Публикация типов

В современном мире существует 2 способа публикации TypeScript типов.

Первый способ - включить .d.ts файлы прямо в библиотеку с .js файлами. Это может быть сделано по разному: можно каждый .d.ts файл хранить рядом с каждым соответствующим .js файлом, а можно внести все .d.ts файлы в отдельную директорию или "сбандлить" в один файл. Преимущество такого подхода в том, что пользоваться этой библиотекой в TypeScript проекте можно "из коробки". Недостаток - .d.ts файлы добавляют размер библиотеке. Если проект не использует TypeScript, то .d.ts файлы ему не нужно. Но из-за того что они встроены в библиотеку, они всегда будут приходить вместе с ней.

Второй способ - выделить типы в отдельную библиотеку. В современном TypeScript сообществе существует подход вынесения типов библиотек в отдельный пакет. Эти пакеты типов хранятся в специальном репозитории: https://github.com/DefinitelyTyped/DefinitelyTyped. В этом репозитории хранятся типы для многих популярных библиотек. Например, здесь хранятся типы для React. Типы из этой библиотеки публикуются в npm с префиксом @types/. Например, типы для React находятся в npm-пакете @types/react. Не обязательно выносить типы своей библиотеки в это репозиторий. Это лишь договоренность. Можно выносить свои типы в отдельный пакет с другим именем. В таком случаи нужно четко объяснить пользователю библиотеки в каком пакете они лежат. Преимущество этого подхода в том, что можно подключать типы тогда, когда они нужны. Недостаток - нужно управлять двумя (или более) пакетами. Если обновили саму библиотеку, нужно не забыть обновить типы. Типы могут не актуальными или обновляться с задержкой.

Задания

Предположим мы написали библиотеку для работы с векторами на JavaScript. Теперь нужно написать TypeScript-типы для этой библиотеки. Библиотека состоит из одного файла: vector.js.

Ответ - vector.d.ts

Last updated