Перейти к содержанию

Декларации

Декларации это очень важная часть TypeScript, благодаря которой магия статической типизации проецируется на динамический JavaScript. Поэтому декларациям будет посвящена вся данная глава, рекомендуемая тем, кто только собирается писать свою первую типизированную библиотеку, которую планируется распространять с помощью npm репозитория.

Что такое декларация (Declaration)

Поскольку при разработке программ на TypeScript используются библиотеки, написанные на JavaScript, компилятор tsc, чьей главной задачей является проверка типов, чувствует себя будто у него завязаны глаза. Несмотря на то, что с каждой новой версией вывод типов все лучше и лучше учится разбирать JavaScript, до идеала ещё далеко. Кроме того, разбор JavaScript кода добавляет нагрузку на процессор, драгоценного времени которого при разработке современных приложений порой и так недостаточно.

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

1
2
3
4
5
6
7
8
Файл Animal.ts


export default class Animal {
   public name: string = 'animal';

   public voice(): void {}
}
1
2
3
4
5
6
7
8
9
Файл Animal.d.ts


declare module "Animal" {
   export default class Animal {
       name: string;
       voice(): void;
   }
}

Еще не забыты дни, когда для часто используемых библиотек приходилось писать декларации вручную, причем они часто содержали ошибки. Кроме этого, декларации не успевали обновляться под постоянно развивающиеся библиотеки. Сейчас такие проблемы кажутся уже нереальными, но несмотря на это, до сих пор приходится прибегать к использованию менеджера деклараций, который был создан в те самые далекие времена.

Установка деклараций с помощью @types

Если декларация распространяется отдельно от библиотеки, то она скорее всего, попадет в огромный репозиторий на github под названием DefinitelyTyped, содержащий огромное количество деклараций. Чтобы было проще ориентироваться в этом множестве, помимо сайта "TypeSearch", выступающего в роли поисковика, был создан менеджер деклараций под названием Typed. Но о нем мы говорить не будем, поскольку он применяется при работе с TypeScript версиями меньше чем v2.0, поэтому речь пойдет о его развитии в образе команды пакетного менеджера npm, а именно @types.

Для того чтобы установить требующуюся декларацию, в терминале необходимо выполнить команду, часть которой состоит из директивы @types, после которой через косую черту / следует имя библиотеки.

1
npm i -D @types/name

Для примера, воспользуемся проектом, созданным в теме, посвященной настройке рабочего окружения, и для демонстрации работы директивы @types установим всем известную библиотеку React.

Первым делом установим саму библиотеку React, выполнив в терминале, запущенном из-под директории проекта, следующую команду.

1
npm i -D react

Открыв директорию /node_modules/ можно убедиться, что библиотека React успешно установлена, поэтому сразу же попытаемся импортировать её в файл index.ts, расположенный в директории src, предварительно изменив его расширение на требуемое для работы с React.tsx.

1
2
3
// @filename: src/index.tsx

import React, { Component } from 'react'; // Error

Несмотря на установленную на предыдущем шаге библиотеку React, при попытке импортировать её модули возникла ошибка. Возникла она потому, что компилятору TypeScript ничего неизвестно о библиотеке React, поскольку декларация, описывающая типы, поставляется отдельно от неё. Чтобы tsc понял, что от него хотят, требуется дополнительно установить декларацию при помощи команды @types пакетного менеджера npm.

1
npm i -D @types/react

Ошибка, возникающая при импорте модулей React исчезла, а если заглянуть в директорию _/node_modules/, то можно увидеть новую примечательную поддиректорию /@types, предназначенную для хранения устанавливаемых с помощью опции @types деклараций.

Но для полноты картины и этого недостаточно. Для того чтобы добавить наш компонент в dom-дерево, необходимо установить ReactDOM, который уже давно развивается отдельной библиотекой.

1
npm i -D react-dom

Кроме того, нужно установить необходимую для работы с ним декларацию.

1
npm i -D @types/react-dom

Осталось только активировать опцию --jsx в _tsconfig.json и скомпилировать проект, как это было показано ранее.

1
2
3
4
5
6
7
8
9
import React, { Component } from 'react'; // Ok
import * as ReactDOM from 'react-dom'; // Ok

const HelloReact = () => <h1>Hello react!</h1>;

ReactDOM.render(
  <HelloReact />,
  document.querySelector('#root')
);

Подготовка к созданию декларации

Помимо того, что декларацию можно написать руками, её также можно сгенерировать автоматически, при условии, что код написан на TypeScript. Для того, чтобы tsc при компиляции генерировал декларации, нужно активировать опцию компилятора --declaration.

Будет нелишним напомнить, что декларацию нужно генерировать только тогда, когда библиотека полностью готова. Другими словами, активировать опцию --declaration нужно в конфигурационном файле production сборки. Кроме того, в декларации нуждается только код, который будет собран в подключаемую библиотеку. Поэтому точкой входа в библиотеку должен быть файл, который содержит только импорты нужных модулей. Но разработка библиотеки невозможна без её запуска, а значит и точки входа, в которой будет создан и инициализирован её экземпляр. Поэтому, чтобы избежать чувства «что-то пошло не так», необходимо помнить, что при создании библиотеки, требующей декларацию, в проекте может быть несколько точек входа. Точкой входа самого компилятора служит конфигурационный файл, который ему был установлен при запуске. Это означает, что если проект находится в директории src, то в декларации путь будет указан как src/libname вместо требуемого lib.

1
2
3
4
5
// Ожидается

declare module 'libname' {
  //...
}
1
2
3
4
5
// Есть

declare module 'src/libname' {
  //...
}

Это в свою очередь означает, что при импорте модулей придется учитывать лишнюю директорию.

1
2
3
// Ожидается

import { libname } from 'libname';
1
2
3
// Есть

import { libname } from 'src/libname';

Это проблему можно решить, разместив конфигурационный файл в директории исходного кода, в нашем случае это директория src. Кто-то не придаст этому значения, кому-то это может показаться неэстетичным. Поэтому при рассмотрении генерации деклараций с помощью tsc, конфигурационный файл будет лежать непосредственно в директории src. Но при рассмотрении генерации деклараций с помощью сторонних библиотек, будет освещен альтернативный вариант.

Но и это ещё не все. Представьте, что Вы создаете библиотеку React, которая в коде представляется одноимённым классом, расположенным в файле React.ts. При этом модуль, который будет представлять вашу библиотеку, должен называться react, что в свою очередь обязывает задать имя файлу, являющемуся точкой входа как `react.js. Ну и что, спросите вы? Если вы ещё не знаете ответ на этот вопрос, то будете удивлены, узнав, что существуют операционные системы, как, например, Windows, которые расценивают пути до файлов React.ts и react.ts идентичными. Простыми словами, если в директории присутствует файл с идентификатором Identifier , то ОС просто не позволит создать одноимённый файл, даже если его символы будут отличаться регистром. Именно об этом и будет сказано в ошибке, возникающей, когда TypeScript обнаружит одноимённые файлы в одной директории. Кроме того, если ваша операционная система позволяет создавать файлы, чьи идентификаторы отличаются только регистром, помните, что разработчик, работающий с вами в одной команде, не сможет даже установить проект себе на машину, если его операционная система работает по другим правилам.

Поэтому решений у этой проблемы, на самом деле, всего два. Задавать идентификаторы, отличающиеся не только регистром. Или же размещать файлы таким образом, чтобы их идентификаторы не пересекались в одной директории. Но при этом нужно помнить, что структура модулей также изменится.

И поскольку TypeScript является компилируемым языком, не будет лишним напомнить правила именования директории, в которую будет компилироваться результат. В случае разработки приложения, директорию, содержащую скомпилированный результат, принято называть dest (сокращение от слова destination). При разработке внешней библиотеки или фреймворка, директорию для собранных файлов принято называть dist (сокращение от слова distributive).

Разновидности деклараций

На самом деле эта глава должна называться «разновидности библиотек», так как именно о них и пойдет речь. Дело в том, что совсем недавно вершиной хорошего тона считалось объединение всего кода в один файл. Это же правило соблюдалось и при создании библиотек. Но сейчас все кардинально поменялось, и дело вот в чем.

В мире JavaScript существует большое количество библиотек, чей размер по меркам клиентских приложений превышает разумный. При этом отказ от них будет означать, что вам самому придется тратить драгоценное время на реализацию части их функционала. Это побудило создателей сборщиков наделять свои творения механизмом, называющимся Tree Shaking.

Tree Shaking — это механизм, позволяющий включать в сборку исключительно используемый код. Простыми словами, данный механизм позволяет взять только используемую часть от всей библиотеки. В перспективе это должно быть спасением, но на деле оказалось не совсем так.

Дело в том, что на данный момент Tree Shaking работает только если библиотека разбита на множество модулей. К примеру, такие именитые библиотеки, как lodash или rxjs, для каждой отдельной функции создают отдельную точку входа, что при их использовании позволяет значительно сократить размер конечного кода. Обозначим подобные библиотеки как библиотеки с множеством точек входа. Кроме того, существуют библиотеки, сопоставимые с монолитом, поскольку при использовании их малой части в конечную сборку они попадают целиком. Обозначим такие библиотеки как библиотеки с единственной точкой входа.

Декларации и область видимости

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Файл ./types/petanimal.d.ts

declare module 'Pig' {
  // Error
  export default class Pig {}
}
declare module 'Goat' {
  // Error
  export default class Goat {}
}
declare module 'petanimal' {
  // Ok
  export { default as Pig } from 'Pig';
  export { default as Goat } from 'Goat';
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Файл ./types/wildanimal.d.ts

declare module 'Pig' {
  // Error
  export default class Pig {}
}
declare module 'Goat' {
  // Error
  export default class Goat {}
}
declare module 'wildanimal' {
  // Ok
  export { default as Pig } from 'Pig';
  export { default as Goat } from 'Goat';
}
1
2
3
4
// Файл index.ts


import Pig from Pig; // From which library should import module?

Погружение в область видимости стоит начать с понимания процесса компилятора, стоящего за установлением принадлежности к декларации в случаях, когда она распространяется не через менеджер @types. Прежде всего, компилятор ищет в файле package.json свойство types и при его отсутствии или пустом значении “” переходит к поиску файла index.d.ts в корне директории. Если свойство types ссылается на конкретную декларацию, то точкой входа считается она. В противном случае файл index.d.ts. Стоит учесть, что при разработке будет возможно только взаимодействовать с модулями, подключенными к точке входа. Кроме того, ограничить область видимости можно при помощи конструкций, объявленных при помощи ключевых слов module или namespace. Единственное, о чем сейчас стоит упомянуть, что области, определяемые обеими конструкциями, нужно расценивать как обычные модули, поскольку они могут включать только одно объявление экспорта по умолчанию (export default).

Если не уделить должного внимания области видимости при создании деклараций для подключаемых библиотек, у разработчиков, которые будут использовать подобные декларации с другими декларациями, имеющими идентичное определение, могут возникнуть ошибки на этапе компиляции. Решений у этой проблемы всего два — сокрытие определений и уточнение определений. Способ, к которому стоит прибегнуть, зависит от вида разрабатываемой библиотеки.

Декларации для библиотек с одной точкой входа

В проекте, созданном в теме, посвященной настройке рабочего пространства, в директории src создайте две точки входа, одну для разработки index.ts, а другую для prod-версии, имя которой должно соответствовать имени библиотеки, в нашем случае это будет index.lib.ts.

По умолчанию точкой входа, как npm пакета, так и декларации, является файл с именем index. Поэтому, если в проект библиотеки имеет несколько точек входа, то важно не забыть указать имя файла с помощью свойства types package.json. Если для сборки используется webpack, то будет значительно проще изменить имя на index во время компиляции.

Кроме того, создайте два файла: IAnimal.ts и Zoo.ts. Также в директории /src создайте директорию /animal, в которой будут размещены два файла: Bird.ts и Fish.ts. В итоге должна получиться следующая структура:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
* /
   * src
      * utils
         * string-util.ts
      * animal
         * Bird.ts
         * Fish.ts
      * IAnimal.ts
      * Zoo.ts
      * index.ts
      * index.lib.ts
      * tsconfig.prod.ts
1
2
3
4
5
// Файл IAnimal.ts

export interface IAnimal {
  name: string;
}
1
2
3
4
5
// Файл utils/string-util.ts

export function toString(text: string): string {
  return `[object ${text}]`;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Файл animals/Bird.ts

import { IAnimal } from '../IAnimal';
import * as StringUtil from '../utils/string-util';

export default class Bird implements IAnimal {
  constructor(readonly name: string) {}

  public toString(): string {
    return StringUtil.toString(this.constructor.name);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Файл animals/Fish.ts

import { IAnimal } from '../IAnimal';
import * as StringUtil from '../utils/string-util';

export default class Fish implements IAnimal {
  constructor(readonly name: string) {}

  public toString(): string {
    return StringUtil.toString(this.constructor.name);
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Файл Zoo.ts

import { IAnimal } from './IAnimal';

export default class Zoo {
  private animalAll: IAnimal[] = [];

  public get length(): number {
    return this.animalAll.length;
  }

  public add(animal: IAnimal): void {
    this.animalAll.push(animal);
  }
  public getAnimalByIndex(index: number): IAnimal {
    return this.animalAll[index];
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Файл index.ts

import Bird from './animals/Bird';
import Fish from './animals/Fish';

import Zoo from './Zoo';

const zoo: Zoo = new Zoo();

zoo.add(new Bird('raven'));
zoo.add(new Fish('shark'));

for (let i = 0; i < zoo.length; i++) {
  console.log(
    `Animal name: ${zoo.getAnimalByIndex(i).name}.`
  );
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Файл index.lib.ts

/** imports */

import { IAnimal } from './IAnimal';
import ZooCollection from './Zoo';

/** re-exports */

export { IAnimal } from './IAnimal'; // type

export { default as Bird } from './animals/Bird'; // type
export { default as Fish } from './animals/Fish'; // type

export { default as Zoo } from './Zoo'; // type

export const zoo: Zoo = new Zoo(); // instance

В коде нет ничего необычного, поэтому комментариев не будет. Если кому-то содержимое файла index.lib.ts показалось необычным, то стоит отметить, что это обычный ре-экспорт модулей JavaScript, который никакого отношения к TypeScript не имеет. Повторю, файл index.lib.ts является точкой входа создаваемой библиотеки, поэтому он должен экспортировать все то, что может потребоваться при работе с ней. Конкретно в этом случае экспортировать utils наружу не предполагается, поэтому они не были реэкспортированы.

Также стоит обратить внимание на конфигурационные файлы TypeScript, которые взаимно добавляют точки входа друг друга в исключение. Кроме того, конфигурационный файл dev-сборки исключает также конфигурационный файл prod-сборки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Файл /src/tsconfig.prod.json


{
   "compilerOptions": {
     "target": "es6",
     "module": "umd",
     "rootDir": "./",
     "declaration": true
   },
   "exclude": [
     "/node_modules",
     "./index.ts"
   ]
 }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//  Файл /tsconfig.json


{
"compilerOptions": {
  "target": "es6",
  "module": "umd",
  "rootDir": "./src"
},
"exclude": [
  "/node_modules",
  "./src/index.lib.ts",
  "./src/tsconfig.prod.json"
]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Файл package.json


{
"name": "zoo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
  "build": "./node_modules/.bin/tsc --project ./tsconfig.json --watch",
  "build:prod": "./node_modules/.bin/tsc --project ./src/tsconfig.prod.json"
},
"author": "",
"license": "ISC",
"devDependencies": {
  "typescript": "^2.5.2"
}
}

Осталось только запустить prod-сборку и, если все было сделано правильно, в директории dist появятся скомпилированные файлы с расширением .js (конечный код) и .d.ts (представляющие декларации, необходимые для работы как самого компилятора TypeScript, так и автодополнения ide).

1
2
3
4
5
// Файл IAnimal.d.ts

export interface IAnimal {
  name: string;
}
1
2
3
// Файл utils/string-util.d.ts

export declare function toString(text: string): string;
1
2
3
4
5
6
7
8
// Файл animals/Bird.d.ts

import { IAnimal } from '../IAnimal';
export default class Bird implements IAnimal {
  readonly name: string;
  constructor(name: string);
  toString(): string;
}
1
2
3
4
5
6
7
8
// Файл animals/Fish.d.ts

import { IAnimal } from '../IAnimal';
export default class Fish implements IAnimal {
  readonly name: string;
  constructor(name: string);
  toString(): string;
}
1
2
3
4
5
6
7
8
9
// Файл Zoo.d.ts

import { IAnimal } from './IAnimal';
export default class Zoo {
  private animalAll;
  readonly length: number;
  add(animal: IAnimal): void;
  getAnimalByIndex(index: number): IAnimal;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Файл index.d.ts

import Zoo from './Zoo';
/** re-exports */
export { IAnimal } from './IAnimal';
export { default as Bird } from './animals/Bird';
export { default as Fish } from './animals/Fish';
export { default as Zoo } from './Zoo';
/** exports */
export declare const zoo: Zoo;

Также стоит сказать, что сгенерированная декларация не может рассматриваться как единоверная. Очень часто можно увидеть декларации, собранные в одном файле и сгруппированные по логическим признакам с помощью namespace или так называемых ghost module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**ghost module */

declare module Zoo {
  interface IAnimal {
    name: string;
  }

  class Bird implements IAnimal {
    readonly name: string;
    constructor(name: string);
    toString(): string;
  }
  class Fish implements IAnimal {
    readonly name: string;
    constructor(name: string);
    toString(): string;
  }

  class Zoo {
    private animalAll;
    readonly length: number;
    add(animal: IAnimal): void;
    getAnimalByIndex(index: number): IAnimal;
  }

  const zoo: Zoo;
}
1
2
3
4
5
/** module */

declare module 'zoo' {
  export = Zoo;
}

Судить, какой из этих вариантов лучше, я не возьмусь, так как на мой взгляд, в данный момент, они оба не являются исчерпывающими. Возможно, в будущем появятся новые правила для создания деклараций, или редакторы будут по-другому обрабатывать эти.

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

Декларации для библиотек с множеством точек входа

При разработке библиотеки, имеющей множество самостоятельных частей, более разумно создавать каждую часть в виде отдельной точки входа. Это позволит использующим её приложениям за счет подключения только необходимых частей минимизировать вес конечной сборки, что становится возможно благодаря механизму Tree Shaking.

Для этого рассмотрим проект, состоящий из самодостаточного модуля bird.ts, который делает реэкспорт модуля Raven.ts, а также самодостаточного модуля fish.ts, реэкспортирующего модуль Shark.ts. Кроме этого, оба модуля доступны в точке входа index.lib.ts.

1
2
3
4
5
* /
   * src/
      * to-string-decorate.ts
      * to-error-decarate.ts
      * index.lib.ts

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

1
2
3
4
5
// Файл to-string-decorate.ts

export function toStringDecorate(type: string): string {
  return `[object ${type}]`;
}
1
2
3
4
5
6
7
8
//Файл to-error-decorate.ts

export function toErrorDecarate(
  message: string,
  id: number = 0
): string {
  return `error:${id === 0 ? '' : id}, ${message}.`;
}
1
2
3
4
5
6
// Файл index.lib.ts

/** re-export */

export { toStringDecorate } from './to-string-decorate';
export { toErrorDecorate } from './to-error-decorate';

После компиляции проекта в директорию dist сгенерируются следующие файлы -

1
2
3
4
5
// Файл to-string-decorate.d.ts

export declare function toStringDecorate(
  type: string
): string;
1
2
3
4
5
6
// Файл to-error-decorate.d.ts

export declare function toErrorDecorate(
  message: string,
  id?: number
): string;
1
2
3
4
5
6
// Файл index.d.ts

/** re-export */

export { toStringDecorate } from './to-string-decorate';
export { toErrorDecorate } from './to-error-decorate';

Сразу следует сказать, что с подобным описанием декларация не будет правильно функционировать, поэтому её придется подправить руками до следующего вида.

1
2
3
4
5
6
7
// Файл to-string-decorate.d.ts

export declare function toStringDecorate(
  type: string
): string;

export as namespace stringDecorate;
1
2
3
4
5
6
7
8
// Файл to-error-decorate.d.ts

export declare function toErrorDecorate(
  message: string,
  id?: number
): string;

export as namespace errorDecorate;
1
2
3
4
5
6
7
8
// Файл index.d.ts

/// <reference path="./to-string-decorate.d.ts" />
/// <reference path="./to-error-decorate.d.ts" />

declare module 'zoo' {
  export default { stringDecorate, errorDecorate };
}

Обычно как отдельную часть принято экспортировать только самодостаточные модули, такие как функции или классы. Но кроме того, могут потребоваться объекты, содержащие константы или что-то незначительное, без чего отдельный модуль не сможет функционировать. Если такие объекты используются всеми самостоятельными модулями, то их можно также вынести в отдельный самостоятельный модуль. В случае, когда самодостаточному модулю для полноценной работы требуются зависимости, которые больше никем не используются, то такой модуль нужно оформлять так же как обычную точку входа. Другими словами, он должен содержать реэкспорт всего необходимого. А кроме того, экспортировать все как глобальный namespace с помощью синтаксиса:

1
export as namespace identifier;

Данный синтаксис объединяет все объявленные экспорты в глобальное пространство имен с указанным идентификатором. Затем объявленные пространства имен нужно импортировать в точку входа с помощью директивы с тройным слешем /// <reference path=””/>, после чего экспортировать из объявленного модуля.

Создание деклараций вручную

Описывать незначительные декларации самостоятельно (вручную) приходится довольно часто. Потребность возникает при необходимости задекларировать импортируемые файлами .ts нестандартные для него расширения файлов. Дело в том, что компилятор TypeScript понимает только импорт расширения .ts/.tsx/.d.ts/.json, а с активной опцией --allowJS, еще и .js/.jsx. Но, работая с таким сборщиком как webpack или используя css-in-js, придется импортировать в код файлы с таким расширением как .html, .css, и т.д. В таких случаях приходится создавать декларации файлов вручную.

Самостоятельно объявление деклараций начинается с создания директории, предназначенной для их хранения. В нашем случае это будет директория types, расположенная в корне проекта. Декларации можно складывать прямо в неё, но будет правильно создавать под каждую декларацию отдельную поддиректорию, носящую имя модуля, нуждающегося в ней. Поэтому создадим поддиректорию с именем /css, а уже в ней создадим файл index.d.ts. Откроем этот файл и напишем в нем декларацию, определяющую расширение .css.

1
2
3
4
5
6
// Файл ./types/css/index.d.ts

declare module '*.css' {
  const content: any;
  export default content;
}

В тех случаях, когда модуль определяет тип any, более уместно использовать при объявлении сокращенный вариант, который предполагает тип any.

1
declare module '*.css';

Осталось только подключить декларацию в конфигурационном файле, и ошибок при импорте расширения .css не возникнет.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Файл tsconfig.json


{
  "compilerOptions": {
      "target": "es2015",
      "module": "none",
      "rootDir": "./src",
      "typeRoots": [
          "./types"
      ]
  },
  "exclude": [
      "./node_modules"
  ]
}

Будет нелишним упомянуть, что самостоятельное создание деклараций, помимо нестандартных расширений, также часто требуется при необходимости расширения типов, описывающих внешние библиотеки. Например, если при работе с библиотекой React возникнет необходимость в использовании пользовательских свойств, определенных спецификацией html, то придется расширять объявляемый в её модуле тип HTMLAttributes.

Директива с тройным слешем (triple-slash directives)

До этого момента было рассмотрено создание библиотек, представленных одним или больше количеством самостоятельных модулей. Акцент в этом предложении необходимо сделать на слове самостоятельных, поскольку они не были зависимы от каких-либо других модулей (деклараций). Если разрабатываемая библиотека представляет из себя множество зависящих друг от друга модулей или она зависит от деклараций, устанавливаемых с помощью директивы @types, то генерируемые декларации также будут нуждаться в зависимостях. Для этих случаев существует директива /// <reference types=””/>. Данная директива указывается в начале файла и предназначена для подключения деклараций, путь до которых указывается с помощью атрибута types.

1
/// <reference types="react" />

Кроме того, с помощью данной директивы можно указать версию используемой библиотеки.

1
/// <reference lib="es2015" />

Подобный функционал будет полезен разработчикам деклараций .d.ts, которые зависят от конкретной версии ECMAScript.

Импортирование декларации (import)

Помимо типов, описанных в глобальных декларациях, в аннотациях типов также можно использовать типы из деклараций, импортированных с помощью директивы import.

1
2
3
4
5
// file declaration-excluded-from-global-scope/animal.d.ts

export declare interface IAnimal {
  type: string;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// file src/index.ts

import * as DTS from 'declaration-excluded-from-global-scope/animal';

// импорт декларации на уровне модуля

let v0: DTS.IAnimal = { type: '' }; // Ok
let v1: DTS.IAnimal = { type: 5 }; // Error

// инлайн импорт

let v2: import('declaration-excluded-from-global-scope/animal').IAnimal = {
  type: '',
}; // Ok
let v3: import('declaration-excluded-from-global-scope/animal').IAnimal = {
  type: 5,
}; // Error

Этот механизм также позволяет указывать аннотацию типов непосредственно в файлах с расширением .js.

1
2
3
4
5
// file declaration-excluded-from-global-scope/animal.d.ts

export declare interface IAnimal {
  type: string;
}
1
2
3
4
5
6
7
// file lib/index.js

/**
*
* @param {import("./declaration-excluded-from-global-scope/animal").IAnimal} animal
*/
export function printAnimalInfo(animal){ animal.type; // autocomplete }
1
2
3
4
5
6
// file src/index.ts

import * as AnimalUtils from 'lib/index.js';

AnimalUtils.printAnimalInfo({ type: '' }); // Ok
AnimalUtils.printAnimalInfo({ type: 5 }); // Error

Комментарии