#7 Модулі, namespace, declare, імпорт та експорт в TypeScript

Імпорт в TS працює практично так само, як і в JS.  Але я декілька нюансів, і перше, що треба зрозуміти що TS компілює TS, та не являється збірником пакетив нак накшталт vitejs. Тому коли ви будете компілювати імпорти, вони будуть перетворені в require. Якщо ж ви хочете збирати проект, то вам потрін вайт, вебпак або ролап, з надналаштуваннями, до них ми дойдемо в наступних уроках.

Імпорт JS

Із коробки TS хоч і розуміє іморт JS, але буде видавати синтаксичні попередження, а в строгому режимі навіть помилки. Для того, щоб цього уникнути, вам в ваш tsconfig.json треба додати параметр "allowJs": true. Тоді TS буде розуміти, що JS модулі дозволені. Але, якщо ви використовуєте TS, мабуть виб захотіли типізувати модуль який ви підключаєте. Така ситуація, може скластися, коли ви підключаєте JS бібліотеку наприклад. Хоча перед тип, як писати велику кількість коду, я би порекомендував би прогуглити підключаємі типи до вашої бібліотеки, тому що багато розробників пишуть їх, і інколи це роблять окремо, наприклад lodash:

npm install lodash
npm install --save-dev @types/lodash

І перше на що треба звернути увагу при встановленні типів, це параметр --save-dev, тому що TS використовується тільки на етапі збірки, тому типи для нього вам в кінцевому пакеті не потрібні, а тільки при розробці.

Якщо пакет типів не містить, і ви хочете написати їх самостійно, то тут є декілька варіантів. 

Деклараційні файли .d.ts

Деклараційні файли це по факту ваше спасіння, щоб не писати в кожному файлі інтерфейси та типи, ви можете їх задекларувати в деклараційних файлах, які можуть лежати рядом з виконуємим файлом TS, або в окремій папці, вказаній через конфіг.

Для початку змінемо ваш конфіг, та троха структуру проекту.

  1. Створіть 3 папки, src, dist, types
  2. Перенесіть ваш index.ts в src
  3. Створіть index.d.ts в папці types
  4. В package.json додайте в script "build": "tsc --build"
  5. Змініть tsconfig.json на:
{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "ESNext",
    "module": "UMD",
    "allowJs": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src", "types"]
}

Include говорить звідки брати типи, а також звідки компілювати файли. Таким чином при виконанні команди build, з пункту 4 вище, всі типи, які знаходяться в папці types підтянутся автоматично. Ви можеие писати оголошувати типи всі в одному файлі, або розбивати на структури які вам потрібні.

Також треба враховувати особливість TS при такому підході. Якщо змінна оголошена в якомусь файлі, то вона стане доступною у всьому проекті, але не попаде в кінцеву збірку. Вся справа в тому, як працює TS. По факту TS не є збірником типу вайта, а лише валідує файли, викристовуючи Runtime. Валідує він ті файли, які ви в ньго включаєете. Тобто, якщо в src ви вказали папку де знаходяться всі файли, то він тупо їх обʼєднає в один, потім почне перевіряти, а потім при збірці розібʼє на потрібні файли. Але якщо в одному файлі ви вказали змінную user, то в іншому файлі, якщо він імортований в include, ви вже не зможете створити або використовувати user, без імпорта чи експорту, тому що TS буде вважати що вона вже є.

Так виходить, тому що Runtime JavaScript, при компілюванні коду, виносить всі let, const, var вгору коду. Тому коли ви створили в одному файлі const user, і в іншому будете намагатися теж створити const user, то user вже буде існувати в горі файлу, і ви отримаєте помилку. Використати цю змінну без іморта ви також не зможете, тому що на етапі збірки, TS не буде виконувати include. Тобто в такій ситуації, TS не буде видавати помилку, що змінної не існує, якщо ви будете намагатися її використати, але віддасть помилку, якщо ви будете намагатися її створити.

Насправді, коли після ви будете працювати з збірниками типу vitejs, vuejs, та подібними, цієї проблеми у вас не буде, тому що збірники пакетів використовують свій Runtime для обробки файлій ts, для них написані свої плагіни. Я б не назвав би це глюком, але ви повинні знати цю особливість, особливо поки вивчаєте TS.

Файл src/index.ts

const user = "Анна";

Файл src/another.ts

console.log(user); // ❌ TypeScript НЕ видає помилку, але у JS це зламається

Скомпільований another.js

"use strict";
console.log(user); // ❌ ReferenceError: user is not defined

Скомпільований index.js

"use strict";
const user = "Анна";

Жесть правда? Але це все через налаштування, якщо ви буде компілити файли поштучно, то такої проблеми не буде. Про це буде в наступних уроках. А покищо, ви можете додати в ваш tsconfig compilerOptions:

{
  "compilerOptions": {
    "moduleDetection": "force"
  }
}

Це по факту вимкне автозлиття, і доможе вам не створювати помилок на початку вивчення. Але додасть до коду самовинуючу функцію, яка буде обертати весь код, і код можна буде використати тільки після додаткової обробки. Тому на етапі вивчення, самі обирайте, що вам зручніше, працювати з декількома файлами, чи додатково працювати з обробкою модулів.

Declare

Декларація в TS необхідня для того, щоб ви могли використати неіснуючі в серидовищі змінні. Базовий приклад - у вас десь в серидовищі, або підключаємий через cdn script src, який передає глобально в код змінну. Самий базовий приклад це cordova, яка свторю глобальну змінну cordova. Вам наприклад її потрібно використати, і ви точно знаєте що вона є, але TS вам видасть помилку, тому що він її ніде не бачить в серидовищі. Для цього і потрібен declare, він говорить, що ця змінна десь є, просто ти її не бачиш. Фактично, declare створюю виключенння для валідації, та додає тип. Наприклад:

console.log(myGlobalVar); // ❌ Помилка: Cannot find name 'myGlobalVar'
declare const myGlobalVar: string;
console.log(myGlobalVar); // помилки нема

При цьому declare не зарееструє змінну в кінечному файлі, тоюто якщо цієї змінної нема, то її нема, декларування говорить TS, що ви точно знаєте що змінна є і TS помиляється )

Декларувати ви можете не тільки змінні, а і будь що, наприклад функції:

declare function logMessage(message: string): void;

Це потрібно, якщо ви імпортували якусь лібу на JS, і хочете її зарееструвати в TS, тоді ви декларуєте її функції, тим самим пишете для вого коду типи цієї бібліотеки.

Declare також можна використовувати для ціли модулів, це вигляжає так:

import * as legacyLib from "legacy-library"; // ❌ Помилка: Cannot find module 'legacy-library'
declare module "legacy-library" { // Вирішення
    export function oldFunction(param: string): void; 
}

Також декларування використовується для створення глобальних типів, ви можете створити файл global.d.ts, додати його в вашу папку types з наступним змістом:

declare global {
    const API_URL: string;
}
export {}; // Запобігає помилці про "Cannot redeclare block-scoped variable"

Тепер ви по факту заделарували змінну для всього проекту.

Namespaces

Якщо ж ви хочете все задекларувати в одному файлі, або створити логічні группи, то ви можете використовувати namespaces, які зможуть згрупувати ваші декларації:

declare namespace MyAPI {
    function getUser(id: number): string;
    const version: string;
}
console.log(MyAPI.getUser(123));
console.log(MyAPI.version); 

Ну і ще раз повторюсь, вам потрібно памʼятати, що declare це чисто інструкції TS, в вихідному js файлі їх не буде. Ось так глобально:

declare global {
  const API_URL: string;
  namespace MyAPI {
    function getUser(id: number): string;
    const version: string;
  }
}
export { }; // Запобігає помилці про "Cannot redeclare block-scoped variable"

Але відмінність namespace від всього іншого, те що ви можете не тільке оголосити тип змінноЇ, але і зарееструвати її відразу, тобто створити таку ефімерну группу змінних, і при такому підході, вони попадуть в JS:

namespace AnotherApp {
  export const user = "Олексій";
}

console.log(AnotherApp.user); // "Олексій"

І хоч це може бути зручним, але підхід namespaces застарів. Його вже мало хто використовує, і знайти його можна тільки в старих модулях. По факту самі розробники TS рекомендують використовувати модульних підхід, а не namespace. Тому розказав я про нього тільки для того, щоб ви розуміли що це таке, коли зустрінете це у когось в коді.

На цьому я думаю все, я не буду вас перегружати, тому зустрінемося в наступному уроці.