Пошук уроків, статей та іншого контенту
Навчитеся описувати типи бібліотек і власного JavaScript-коду за допомогою declaration files та файлів .d.ts.
Declaration file — це файл із описом типів без реалізації коду. У TypeScript такі файли зазвичай мають розширення .d.ts.
У declaration file можна описати:
функції;
змінні;
класи;
інтерфейси;
типи;
властивості об’єктів;
експорт модуля;
API JavaScript-бібліотеки.
Файл .d.ts не містить виконуваного коду. Він лише повідомляє TypeScript, які значення доступні та як їх використовувати.
Наприклад, файл logger.d.ts може описувати модуль:
export function log(message: string): void;
export function error(message: string, code?: number): void;Цей файл не реалізує функції log та error. Він лише описує їхні типи.
.d.tsTypeScript перевіряє типи лише там, де він бачить типову інформацію. Для TypeScript-коду типи зазвичай виводяться безпосередньо з .ts-файлів.
Але типової інформації може не бути в таких випадках:
бібліотека написана на JavaScript;
власний старий код написаний на JavaScript;
пакет не містить типів;
типи потрібно відокремити від реалізації;
глобальний API надається середовищем виконання.
Declaration files дозволяють використовувати такий код із перевіркою типів:
import { calculateTotal } from "legacy-shop";
const total = calculateTotal(100, 20);Якщо TypeScript знає опис calculateTotal, він перевірить типи аргументів і результату, навіть якщо реальна функція написана на JavaScript.
Оголошення без реалізації називають ambient declarations. Для них часто використовується ключове слово declare.
declare function getCurrentUserId(): string;Це означає:
Функція
getCurrentUserIdіснує десь у середовищі виконання, але її реалізація знаходиться поза цим файлом.
TypeScript дозволить викликати функцію та перевірятиме її тип:
const userId = getCurrentUserId();
// userId має тип stringАле під час виконання JavaScript-функція справді повинна існувати. Declaration file не створює її автоматично.
Якщо викликати функцію, якої немає в реальному середовищі, TypeScript може успішно скомпілювати код, але програма завершиться помилкою під час виконання.
У declaration file можна оголошувати звичайні TypeScript-конструкції:
interface Product {
id: number;
name: string;
price: number;
}
declare function findProduct(id: number): Product | undefined;Інтерфейс Product описує форму об’єкта, а функція findProduct оголошена без реалізації.
Можна також експортувати типи та функції з declaration file:
export interface Product {
id: number;
name: string;
price: number;
}
export function findProduct(id: number): Product | undefined;Такий файл описує модуль, який має іменований експорт Product та функцію findProduct.
Розглянемо JavaScript-файл, який потрібно використовувати в TypeScript-проєкті.
Файл legacy-math.js:
function addTax(price, taxRate) {
return price + price * taxRate;
}
function formatPrice(price, currency) {
return `${price.toFixed(2)} ${currency}`;
}
module.exports = {
addTax,
formatPrice,
};TypeScript не знає типів цих функцій. Створимо поруч declaration file legacy-math.d.ts:
export function addTax(price: number, taxRate: number): number;
export function formatPrice(
price: number,
currency: string,
): string;Тепер TypeScript зможе перевіряти використання модуля:
import { addTax, formatPrice } from "./legacy-math";
const total = addTax(100, 0.2);
const formatted = formatPrice(total, "UAH");
console.log(formatted);Помилкові виклики будуть виявлені під час перевірки:
import { addTax } from "./legacy-math";
addTax("100", 0.2);
// Помилка: перший аргумент має бути numberproject/
├── app.ts
├── legacy-math.js
├── legacy-math.d.ts
└── tsconfig.jsonФайл tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["app.ts", "legacy-math.d.ts"]
}Скомпілювати TypeScript-код можна командою:
tscПід час запуску dist/app.js файл legacy-math.js також повинен бути доступний у відповідному місці, адже .d.ts описує модуль, але не замінює його реалізацію.
declare moduleІноді неможливо або незручно створити .d.ts поруч із JavaScript-файлом. Наприклад, потрібно описати зовнішню бібліотеку, яка не має типів.
У такому випадку можна створити окремий файл, наприклад types/legacy-shop.d.ts:
declare module "legacy-shop" {
export interface Order {
id: string;
total: number;
paid: boolean;
}
export function calculateTotal(
subtotal: number,
deliveryPrice: number,
): number;
export function getOrder(id: string): Promise<Order>;
}Тепер TypeScript знає, що модуль "legacy-shop" має такі експорти:
import {
calculateTotal,
getOrder,
} from "legacy-shop";
const total = calculateTotal(500, 80);
async function printOrder(orderId: string) {
const order = await getOrder(orderId);
console.log(order.id);
console.log(order.total);
console.log(order.paid);
}
printOrder("order-42");Шлях до папки з declaration files має входити до області, яку перевіряє TypeScript. Наприклад:
{
"compilerOptions": {
"strict": true,
"module": "CommonJS",
"target": "ES2020"
},
"include": ["src", "types"]
}Якщо папка types не вказана в include, TypeScript може не знайти створене оголошення.
Для CommonJS-модуля можна використовувати export =.
JavaScript-реалізація:
function createId(prefix) {
return `${prefix}-${Date.now()}`;
}
module.exports = createId;Declaration file:
declare function createId(prefix: string): string;
export = createId;Імпорт такого модуля в TypeScript:
import createId = require("./create-id");
const id = createId("user");
console.log(id);Форма declaration file має відповідати фактичному способу експорту модуля:
module.exports = value описується через export =;
іменовані експорти описуються через export function, export const, export interface тощо.
Declaration file може описувати не лише функції, а й складні об’єкти.
JavaScript-модуль:
const config = {
apiUrl: "https://api.example.test",
retryCount: 3,
isProduction() {
return false;
},
};
module.exports = config;Declaration file:
interface AppConfig {
apiUrl: string;
retryCount: number;
isProduction(): boolean;
}
declare const config: AppConfig;
export = config;Використання:
import config = require("./config");
console.log(config.apiUrl);
console.log(config.retryCount);
console.log(config.isProduction());TypeScript перевірить доступ до властивостей:
import config = require("./config");
config.retryCount.toUpperCase();
// Помилка: у number немає методу toUpperCaseІноді JavaScript-код або середовище виконання надає глобальне значення, якого немає у стандартних типах проєкту.
Наприклад, певний скрипт створює глобальний об’єкт appEnvironment. Його можна описати так:
interface AppEnvironment {
apiUrl: string;
version: string;
debug: boolean;
}
declare const appEnvironment: AppEnvironment;Після цього TypeScript дозволить:
console.log(appEnvironment.apiUrl);
console.log(appEnvironment.version);
if (appEnvironment.debug) {
console.log("Debug mode enabled");
}Глобальне оголошення не створює змінну. Воно лише описує змінну, яка вже має бути створена іншим скриптом або середовищем виконання.
Щоб файл із глобальними оголошеннями не став модулем випадково, у ньому не повинно бути import або export на верхньому рівні.
.ts і .d.ts.tsМоже містити:
реалізацію функцій;
виконуваний код;
імпорти;
експорти;
типи.
export function double(value: number): number {
return value * 2;
}.d.tsМістить лише опис:
export function double(value: number): number;У declaration file після сигнатури функції ставиться крапка з комою, а тіло функції відсутнє.
Не потрібно писати реалізацію:
// Неправильно для declaration file
export function double(value: number): number {
return value * 2;
}Declaration files призначені для опису вже наявного коду.
TypeScript автоматично враховує .d.ts файли, якщо вони:
входять до include у tsconfig.json;
доступні через files;
розташовані в місці, яке TypeScript перевіряє;
постачаються разом із пакетом як його типи.
Для власних declaration files найпростіше явно додати папку до include:
{
"include": ["src/**/*.ts", "types/**/*.d.ts"]
}Якщо declaration file лежить поруч із модулем, TypeScript зазвичай знаходить його за назвою модуля. Наприклад:
src/
├── legacy-api.js
├── legacy-api.d.ts
└── app.tsІмпорт:
import { getUser } from "./legacy-api";Файл legacy-api.d.ts описує модуль ./legacy-api.
Зовнішня бібліотека може:
містити власні declaration files;
використовувати окремий пакет типів;
не мати типів узагалі.
Якщо бібліотека не має типів, її API можна тимчасово описати власним declaration file:
declare module "analytics-client" {
export interface EventData {
name: string;
properties?: Record<string, string | number | boolean>;
}
export function track(event: EventData): void;
}Використання:
import { track } from "analytics-client";
track({
name: "button_clicked",
properties: {
button: "buy",
position: 2,
},
});Чим точнішим буде опис, тим більше помилок TypeScript зможе знайти.
Небажано одразу описувати всю бібліотеку як any:
declare module "analytics-client" {
const client: any;
export default client;
}Такий опис прибирає більшість переваг статичної типізації. Краще описати хоча б ті функції та дані, які реально використовуються.
any у declaration filesІноді структура JavaScript API невідома або змінюється. У такому разі any може бути тимчасовим рішенням:
declare module "legacy-client" {
export function request(
url: string,
options?: any,
): Promise<any>;
}Але any вимикає перевірку типів:
const result = await request("/users");
result.thisPropertyMayNotExist();
result.value.withAnyMethod();Краще поступово замінювати any конкретними типами:
interface RequestOptions {
method?: "GET" | "POST";
headers?: Record<string, string>;
}
interface User {
id: number;
name: string;
}
declare module "legacy-client" {
export function request(
url: string,
options?: RequestOptions,
): Promise<User[]>;
}TypeScript не перевіряє автоматично, чи відповідає .d.ts фактичному JavaScript-коду.
Наприклад, реалізація повертає рядок:
function getVersion() {
return "1.0.0";
}
module.exports = {
getVersion,
};А declaration file помилково описує число:
export function getVersion(): number;TypeScript довірятиме declaration file:
import { getVersion } from "./version";
const version = getVersion();
// TypeScript вважає version числомАле під час виконання це буде рядок.
Тому declaration file потрібно підтримувати синхронно з реалізацією:
перевіряти назви експортів;
перевіряти типи аргументів;
перевіряти типи результатів;
оновлювати декларації після змін JavaScript API.
.d.tsexport function sum(a: number, b: number): number {
return a + b;
}У declaration file має бути лише сигнатура:
export function sum(a: number, b: number): number;declare створює значенняdeclare const API_URL: string;
console.log(API_URL);Це не створює API_URL. Під час виконання змінна повинна бути визначена окремим JavaScript-кодом.
Якщо JavaScript використовує:
module.exports = createClient;не слід описувати його як набір іменованих експортів:
export function createClient(): Client;Потрібно використовувати:
declare function createClient(): Client;
export = createClient;Якщо TypeScript не бачить файл types/library.d.ts, імпорт може завершитися помилкою:
Could not find a declaration file for module ...Перевірте include, files або розташування файлу в проєкті.
anyОпис на кшталт:
declare module "some-library" {
const value: any;
export default value;
}прибирає типову перевірку. Варто описувати конкретні частини API, які використовуються в застосунку.
.d.ts і JavaScriptЯкщо declaration file говорить одне, а реалізація робить інше, компілятор не обов’язково це виявить. .d.ts має бути точним контрактом реального коду.
Щоб описати JavaScript-модуль у TypeScript:
Визначте фактичні експорти JavaScript-модуля.
Випишіть параметри його функцій.
Визначте типи результатів.
Опишіть об’єкти через interface або type.
Відобразіть спосіб експорту: export або export =.
Розмістіть .d.ts поруч із модулем або в окремій папці типів.
Переконайтеся, що TypeScript включає цей файл.
Перевірте використання модуля в .ts-коді.
Оновлюйте декларацію разом зі змінами JavaScript API.
Declaration file має розширення .d.ts.
.d.ts описує типи, але не містить реалізації.
declare повідомляє TypeScript про вже наявні значення.
Власний JavaScript-код можна типізувати через .d.ts, розташований поруч із модулем.
API бібліотеки без типів можна описати через declare module.
Для CommonJS-експорту module.exports = value використовується export =.
Declaration file має точно відповідати реальній JavaScript-реалізації.
any у деклараціях слід використовувати обмежено.
.d.ts не створює функції, змінні або об’єкти під час виконання.