Пошук уроків, статей та іншого контенту
Налаштуєте TypeScript-проєкти з Vite, Next.js і Node.js та розберете відмінності їхніх збірок і середовищ виконання.
TypeScript не є окремим середовищем виконання. Браузер і Node.js не запускають TypeScript-файли безпосередньо. Спочатку код потрібно:
перевірити типізатором;
перетворити на JavaScript;
за потреби зібрати в один або кілька бандлів;
запустити у відповідному середовищі.
Vite, Next.js і Node.js розв’язують ці задачі по-різному:
Vite збирає клієнтський застосунок для браузера.
Next.js збирає серверну і клієнтську частини застосунку.
Node.js зазвичай запускає JavaScript на сервері, а TypeScript компілюють окремою командою.
Одна й та сама конструкція TypeScript може мати різні обмеження залежно від середовища. Наприклад:
const value = process.env.API_URL;працює в Node.js і на серверній частині Next.js, але не визначає змінну середовища у звичайному браузерному коді Vite.
Vite має готовий шаблон для TypeScript:
npm create vite@latest vite-app -- --template vanilla-ts
cd vite-app
npm install
npm run devДля React-проєкту використовуйте шаблон react-ts:
npm create vite@latest vite-react-app -- --template react-tsТиповий package.json містить такі скрипти:
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
}
}Команда build складається з двох незалежних етапів:
tsc -b перевіряє TypeScript-проєкт;
vite build створює production-бандл.
Vite відповідає за трансформацію і збирання коду, але сам процес збирання не повинен розглядатися як повна перевірка типів. Тому перевірка через tsc має залишатися окремим етапом.
У Vite-проєкті часто є кілька конфігурацій TypeScript, наприклад для коду застосунку і для конфігураційних файлів. Спрощений tsconfig.json для браузерного коду може виглядати так:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src"]
}Важливі параметри:
lib додає типи браузерних API, зокрема window, document і fetch;
module: "ESNext" залишає модулі у форматі ES Modules для подальшої обробки Vite;
moduleResolution: "Bundler" відповідає правилам пошуку модулів сучасними бандлерами;
noEmit: true забороняє tsc створювати JavaScript-файли;
isolatedModules: true вимагає, щоб кожен файл можна було трансформувати незалежно.
Vite замінює змінні середовища під час збирання. У клієнтський код потрапляють лише змінні з префіксом VITE_.
Файл .env:
VITE_API_URL=https://api.example.com
API_SECRET=не-повинна-потрапити-в-браузерДоступ до змінних:
const apiUrl = import.meta.env.VITE_API_URL;
console.log(apiUrl);API_SECRET не буде доступна через import.meta.env. Однак це не означає, що її безпечно зберігати в .env клієнтського проєкту: секрети взагалі не повинні бути частиною клієнтської збірки.
Для типізації власних змінних створюють декларацію, наприклад src/vite-env.d.ts:
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_FEATURE_CHECKOUT: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}Приклад клієнтського коду:
const apiUrl = import.meta.env.VITE_API_URL;
const checkoutEnabled = import.meta.env.VITE_FEATURE_CHECKOUT === "true";
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<p>API: ${apiUrl}</p>
<p>Checkout: ${checkoutEnabled ? "увімкнено" : "вимкнено"}</p>
`;Після виконання:
npm run buildVite створює каталог dist. У ньому містяться:
HTML;
JavaScript-бандли;
CSS;
статичні ресурси.
Ці файли не потребують Node.js під час виконання. Їх можна віддавати будь-яким статичним вебсервером.
Отже, TypeScript у Vite використовується під час розробки та збирання, але в браузер потрапляє вже JavaScript.
Новий Next.js-проєкт із TypeScript і App Router можна створити так:
npx create-next-app@latest next-app --ts --app
cd next-app
npm run devNext.js автоматично налаштовує TypeScript. У типовому проєкті з’являються:
tsconfig.json;
типи для Next.js;
скрипти dev, build і start;
підтримка файлів .ts і .tsx.
Основні скрипти:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}Production-процес має два етапи:
npm run build
npm run startnext build створює production-артефакти, а next start запускає production-сервер Next.js.
Next.js може виконувати код у різних середовищах:
на сервері;
у браузері;
під час попереднього рендерингу;
під час обробки запитів.
У App Router компоненти без директиви "use client" є серверними компонентами за замовчуванням.
type Product = {
id: string;
name: string;
};
const products: Product[] = [
{ id: "1", name: "Keyboard" },
{ id: "2", name: "Mouse" }
];
export default function ProductsPage() {
return (
<main>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</main>
);
}Такий компонент може використовувати серверні можливості, але не має напряму звертатися до браузерних API, наприклад window або localStorage.
Для інтерактивності потрібен клієнтський компонент:
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button type="button" onClick={() => setCount((value) => value + 1)}>
Лічильник: {count}
</button>
);
}Директива "use client" має бути на початку файлу. Вона визначає межу клієнтської частини та впливає на те, який код потрапить у браузерний бандл.
Next.js зазвичай генерує tsconfig.json автоматично. Приклад важливих параметрів:
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["node_modules"]
}Next.js не використовує tsc для створення JavaScript-файлів застосунку. Параметр noEmit: true залишає TypeScript відповідальним переважно за перевірку типів, а трансформацію і збирання виконує сам Next.js.
Серверний код може читати звичайні змінні:
const databaseUrl = process.env.DATABASE_URL;У клієнтський бандл можуть потрапити тільки змінні з префіксом NEXT_PUBLIC_:
DATABASE_URL=postgres://localhost/app
NEXT_PUBLIC_API_URL=https://api.example.comexport default function ApiStatus() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
return <p>API: {apiUrl}</p>;
}Секретну змінну DATABASE_URL не можна читати в клієнтському компоненті. Якщо значення потрібне браузеру, воно має бути явно призначене для публікації, а назва повинна починатися з NEXT_PUBLIC_.
Оскільки публічні змінні вбудовуються під час збирання, зміна .env після next build не змінить уже створений клієнтський бандл без нового збирання.
На відміну від Vite і Next.js, Node.js сам по собі не є TypeScript-бандлером. Типовий production-процес:
tsc перевіряє і компілює .ts;
Node.js запускає створені .js-файли.
Для зручного запуску TypeScript у розробці можна використати tsx.
Встановлення залежностей:
npm init -y
npm install -D typescript tsx @types/nodeФайл package.json:
{
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"typecheck": "tsc --noEmit",
"build": "tsc",
"start": "node dist/index.js"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest"
}
}Для сучасного Node.js-проєкту можна використати NodeNext:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"]
}Поле "type": "module" у package.json означає, що Node.js розглядає створені файли як ES Modules.
Приклад повністю runnable-сервера:
import { createServer } from "node:http";
const port = Number(process.env.PORT ?? 3000);
const server = createServer((request, response) => {
if (request.url === "/health") {
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ status: "ok" }));
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(JSON.stringify({ error: "Not found" }));
});
server.listen(port, () => {
console.log(`Сервер запущено на порту ${port}`);
});Структура:
src/
index.ts
package.json
tsconfig.jsonЗапуск у режимі розробки:
npm run devПеревірка типів без створення файлів:
npm run typecheckProduction-збірка і запуск:
npm run build
npm startПісля npm run build файл src/index.ts перетворюється на dist/index.js.
Node.js API не належать до стандартних типів TypeScript для браузера. Пакет @types/node додає типи для:
process;
Buffer;
модуля node:http;
файлової системи;
інших Node.js API.
Якщо проєкт є серверним, але TypeScript не бачить process або імпорт node:fs, перевірте, чи встановлено @types/node і чи включені Node.js-типи в конфігурацію.
Для змінних середовища тип process.env.PORT має вигляд string | undefined. Саме тому в прикладі використано значення за замовчуванням:
const port = Number(process.env.PORT ?? 3000);Якщо змінна є обов’язковою, краще перевірити її явно:
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Відсутня змінна середовища: ${name}`);
}
return value;
}
const databaseUrl = requireEnv("DATABASE_URL");Vite створює клієнтський бандл:
TypeScript → JavaScript → браузерний бандлОсобливості:
код виконується в браузері;
використовуються DOM-типи;
доступ до конфігурації відбувається через import.meta.env;
VITE_*-змінні стають частиною клієнтського коду;
tsc зазвичай запускається окремо для перевірки типів;
результатом є статичний каталог dist.
Next.js створює кілька категорій результатів:
TypeScript/TSX → серверна збірка
клієнтська збірка
статичний HTML або інші артефактиОсобливості:
частина коду виконується на сервері;
частина коду потрапляє в браузер;
межа між частинами визначається, зокрема, директивою "use client";
серверні змінні середовища не повинні потрапляти в клієнтські компоненти;
production-збірка запускається через next start;
типи перевіряються в межах процесу Next.js або окремою командою tsc --noEmit.
Node.js-проєкт зазвичай не створює бандл:
TypeScript → JavaScript-файли → Node.jsОсобливості:
код виконується в Node.js;
доступні process, файлові API та серверні модулі;
tsc створює JavaScript у outDir;
Node.js запускає створені файли;
для розробки потрібен окремий TypeScript-рантайм або попередня компіляція;
формат модулів має узгоджуватися між package.json і tsconfig.json.
Поширена причина помилок у Node.js — змішування CommonJS та ES Modules.
package.json:
{
"type": "module"
}tsconfig.json:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}У коді використовують import і export:
import { readFile } from "node:fs/promises";
export async function loadConfig(path: string) {
return readFile(path, "utf8");
}Для CommonJS-конфігурації використовують:
{
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node"
}
}У такому випадку Node.js очікує CommonJS-модулі, а TypeScript перетворює import та export відповідно до цього формату.
Головне правило: не вибирайте module ізольовано. Формат TypeScript, поле "type" у package.json і спосіб запуску Node.js повинні описувати одну модель модулів.
Типи можна спільно використовувати між Vite, Next.js і Node.js:
export type User = {
id: string;
email: string;
};Але спільний тип не робить спільною реалізацію. Файл, який імпортується в браузерний код, не повинен містити:
process без відповідної браузерної заміни;
fs;
node:http;
секрети;
серверні ключі доступу.
Наприклад, цей тип безпечний для імпорту і на сервері, і в браузері:
export type ApiUser = {
id: string;
displayName: string;
};А ця реалізація призначена лише для Node.js:
import { readFile } from "node:fs/promises";
export async function readUsers(path: string) {
const content = await readFile(path, "utf8");
return JSON.parse(content);
}Типи можуть бути спільними, але імпорти та виконуваний код потрібно розділяти за середовищами.
Vite може успішно зібрати код, у якому є помилка TypeScript, якщо окремо не запускається tsc.
Використовуйте окрему команду:
npx tsc --noEmitА в package.json об’єднайте перевірку та збірку:
{
"scripts": {
"build": "tsc -b && vite build"
}
}process.env у браузерному Vite-кодіУ Vite клієнтські змінні читають через:
import.meta.env.VITE_API_URLа не через:
process.env.API_URLУсе, що потрапило до клієнтської збірки Vite або Next.js, користувач може переглянути в браузері. Префікс VITE_ або NEXT_PUBLIC_ не захищає значення, а лише позначає його як доступне клієнту.
Серверний компонент не може безпосередньо використовувати window, document або localStorage. Інтерактивну логіку потрібно винести в компонент із "use client".
.ts напряму через Node.jsКоманда:
node src/index.tsне є стандартним способом запуску TypeScript у Node.js. Спочатку скомпілюйте проєкт:
npm run build
node dist/index.jsабо використайте інструмент для розробки, наприклад скрипт із tsx.
module і "type"Якщо TypeScript створює ES Modules, а Node.js очікує CommonJS, або навпаки, виникають помилки імпорту. Перевіряйте разом:
compilerOptions.module;
compilerOptions.moduleResolution;
поле "type" у package.json;
команду запуску.
Якщо TypeScript не знаходить process або node:http, встановіть:
npm install -D @types/nodeVite збирає TypeScript-код для браузера у статичний клієнтський бандл.
У Vite типи потрібно перевіряти окремим запуском tsc, якщо це не налаштовано в build-скрипті.
Next.js поєднує серверну і клієнтську збірки.
У Next.js серверні та клієнтські компоненти мають різні можливості й обмеження.
У Vite для клієнта використовують import.meta.env.VITE_*.
У Next.js серверні змінні читають через process.env, а публічні клієнтські мають префікс NEXT_PUBLIC_.
Node.js не компілює TypeScript автоматично в типовому production-процесі.
Для Node.js потрібно узгодити tsconfig.json, "type" у package.json і формат модулів.
Спільні типи можна використовувати в різних середовищах, але серверну реалізацію не можна імпортувати в браузерний код.