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

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

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

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

// Файл Animal.ts

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

  public voice(): void {}
}
// Файл Animal.d.ts

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

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

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

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

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

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

npm i -D @types/name

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

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

npm i -D react

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

// Файл src/index.tsx

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

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

npm i -D @types/react

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

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

npm i -D react-dom

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

npm i -D @types/react-dom

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

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 нужно в конфигурационном файле prod сборки.

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

Кроме того, точкой входа самого компилятора служит конфигурационный файл, который ему был установлен при запуске. Это означает, что если проект находится в директории src, то в декларации путь будет указан как “src/libname” вместо требуемого “lib”.

// Ожидается

declare module 'libname' {
  //...
}
// Есть

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

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

// Ожидается

import { libname } from 'libname'
// Есть

import { libname } from 'src/libname'

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

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

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

Также следует обратить внимание на правила именования директории, в которую будут компилироваться файлы. Для этого рассмотрим пример, приведенный в разделе «Настройка рабочего окружения», но с одной модификацией. Дело в том, что в случае, когда разрабатывается приложение, директорию, в которую складываются завершенные файлы, принято называть dest (сокращение от слова destination). В случае разработки внешней библиотеки или фреймворка, директорию для собранных файлов принято называть dist (сокращение от слова distributive).

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

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

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

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

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

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

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

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

// Файл ./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'
}
// Файл ./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'
}
// Файл index.ts

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

Начать понимание области видимости декларации стоит с того, как именно компилятор определяет, что есть декларация в тех случаях, когда она распространяется не через npm. Прежде всего он ищет в файле package.json свойство types и если оно не определено или имеет значение “”, то компилятор ищет файл с именем index.d.ts в корне директории, который расценивается в качестве точки входа. В случае, если свойство types ссылается на конкретную декларацию, то точкой входа считается она. Разработчик может взаимодействовать только с теми модулями, до которых можно проложить путь от точки входа.

Кроме того, ограничить область видимости можно при помощи module или namespace. Единственное, о чем сейчас стоит упомянуть, что как module, так и namespace нужно расценивать как обычную область видимости, так как в их описании не может присутствовать больше одного объявления экспорта по умолчанию (export default).

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

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

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

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

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

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

* /
   * src
      * utils
         * string-util.ts
      * animal
         * Bird.ts
         * Fish.ts
      * IAnimal.ts
      * Zoo.ts
      * index.ts
      * index.lib.ts
      * tsconfig.prod.ts
// Файл IAnimal.ts

export interface IAnimal {
  name: string
}
// Файл utils/string-util.ts

export function toString(text: string): string {
  return `[object ${text}]`
}
// Файл 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)
  }
}
// Файл 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)
  }
}
// Файл 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]
  }
}
// Файл 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}.`)
}
// Файл 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-сборки.

// Файл /src/tsconfig.prod.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "umd",
    "rootDir": "./",
    "declaration": true
  },
  "exclude": ["/node_modules", "./index.ts"]
}
//  Файл /tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "umd",
    "rootDir": "./src"
  },
  "exclude": ["/node_modules", "./src/index.lib.ts", "./src/tsconfig.prod.json"]
}
// Файл 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.

// Файл IAnimal.d.ts

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

export declare function toString(text: string): string
// Файл animals/Bird.d.ts

import { IAnimal } from '../IAnimal'
export default class Bird implements IAnimal {
  readonly name: string
  constructor(name: string)
  toString(): string
}
// Файл animals/Fish.d.ts

import { IAnimal } from '../IAnimal'
export default class Fish implements IAnimal {
  readonly name: string
  constructor(name: string)
  toString(): string
}
// Файл Zoo.d.ts

import { IAnimal } from './IAnimal'
export default class Zoo {
  private animalAll
  readonly length: number
  add(animal: IAnimal): void
  getAnimalByIndex(index: number): IAnimal
}
// Файл 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.

/**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
}
/** module */

declare module 'zoo' {
  export = Zoo
}

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

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

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

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

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

* /
   * src/
      * to-string-decorate.ts
      * to-error-decarate.ts
      * index.lib.ts

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

// Файл to-string-decorate.ts

export function toStringDecorate(type: string): string {
  return `[object ${type}]`
}
//Файл to-error-decorate.ts

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

/** re-export */

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

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

// Файл to-string-decorate.d.ts

export declare function toStringDecorate(type: string): string
// Файл to-error-decorate.d.ts

export declare function toErrorDecorate(message: string, id?: number): string
// Файл index.d.ts

/** re-export */

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

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

// Файл to-string-decorate.d.ts

export declare function toStringDecorate(type: string): string

export as namespace stringDecorate
// Файл to-error-decorate.d.ts

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

export as namespace errorDecorate
// Файл 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 с помощью синтаксиса:

export as namespace identifier

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

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

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

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

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

// Файл ./types/css/index.d.ts

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

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

declare module '*.css'

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

// Файл tsconfig.json

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

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

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

Тогда, когда разработчик создает библиотеку, зависящую от другой библиотеки, как например react-dom зависит от React, не будет лишнем указать зависимость при помощи директивы /// <reference types=””/>. Данная директива, в кавычках которой указывается имя библиотеки, располагается в начале файла, который представляет точкой входа.

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

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

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

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

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

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

// file declaration-excluded-from-global-scope/animal.d.ts

export declare interface IAnimal {
  type: string
}
// 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.

// file declaration-excluded-from-global-scope/animal.d.ts

export declare interface IAnimal {
  type: string
}
// file lib/index.js

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

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

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