Продакшн та Екосистема: Advanced Config & Workflow
Розділ 5: Advanced Patterns та Конфігурація
Ми вивчили синтаксис. Тепер поговоримо про інфраструктуру. TypeScript — це не тільки мова, це компілятор, і від його налаштування залежить ваше життя (і швидкість CI/CD).
У цьому фінальному розділі ми розберемо все, що потрібно для створення серйозних проектів та бібліотек.
1. Анатомія tsconfig.json
Більшість людей копіює цей файл зі StackOverflow. Давайте зрозуміємо, що там відбувається.
Основні секції
{
"compilerOptions": {
/* Basic Options */
"target": "ES2022", // Яку версію JS генерувати
"module": "NodeNext", // Яку саме систему модулів використовувати (CommonJS vs ESM)
"lib": ["DOM", "ESNext"], // Які глобальні типи доступні (window, document, Array.flat)
/* Strictness */
"strict": true, // Вмикає купу перевірок
"noImplicitReturns": true, // Заставляє повертати значення з усіх гілок функції
"noUncheckedIndexedAccess": true, // Робить доступ по індексу (arr[0]) -> T | undefined
/* Emit */
"outDir": "./dist", // Куди класти .js файли
"rootDir": "./src", // Де лежать .ts файли
"declaration": true, // Генерувати .d.ts файли (для бібліотек)
"sourceMap": true, // Для дебаггингу
"removeComments": true // Видаляти коментарі з JS
},
"include": ["src/**/*"], // Що компілювати
"exclude": ["node_modules", "**/*.test.ts"] // Що ігнорувати
}
noUncheckedIndexedAccess
Це налаштування, яке я рекомендую вмикати всім.
// Без нього
const users: User[] = []
const user = users[0] // Тип: User. Реальність: undefined.
// user.name -> CRASH
// З ним (true)
const user = users[0] // Тип: User | undefined.
// user.name -> Error: Object is possibly 'undefined'.
Це рятує від тисяч багів.
2. Монорепозиторії та Project References
Великі проекти розбивають на частини. Замість одного гігантського tsconfig.json, ми використовуємо композицію.
Структура папок
/repo
/packages
/core
tsconfig.json
/ui
tsconfig.json (depends on core)
tsconfig.json (root)
Налаштування
Core (packages/core/tsconfig.json):
{
"compilerOptions": {
"composite": true, // Дозволяє іншим проектам посилатися на цей
"declaration": true
}
}
UI (packages/ui/tsconfig.json):
{
"references": [{ "path": "../core" }]
}
Root (tsconfig.json):
{
"files": [],
"references": [{ "path": "./packages/core" }, { "path": "./packages/ui" }]
}
Тепер, коли ви міняєте core, TypeScript (в режимі --build) перекомпілює тільки залежні частини ui, а не весь проект. Це прискорює збірку в рази.
3. Namespaces vs Modules
Це джерело плутанини.
Modules (import/export)
Це стандарт ES6. Кожен файл — це модуль. Використовуйте це у 99% випадків.
Namespaces (namespace X { ... })
Це старий спосіб TS групувати код у глобальному об'єкті (аналог IIFE).
namespace Validation {
export const emailRegex = /.../
}
// Validation.emailRegex
Коли використовувати Namespaces?
Тільки для написання типів (.d.ts) для старих бібліотек, які використовують глобальні змінні (наприклад, jQuery плагіни).
4. Declaration Files (.d.ts)
Це файли, які містять тільки типи, без коду. Вони дозволяють TS розуміти JS код.
Ambient Declarations (declare)
Уявіть, що ви підключили бібліотеку super-lib через CDN (глобальна змінна SuperLib).
TS про неї не знає. Дайте йому підказку.
// globals.d.ts
declare const SuperLib: {
init(config: any): void
version: string
}
Розширення існуючих модулів (Module Augmentation)
Ви хочете додати поле до Request в Express.
// express.d.ts
import { User } from './user'
declare module 'express-serve-static-core' {
interface Request {
user?: User // Ми додали це поле!
}
}
Тепер req.user працюватиме у всьому проекті.
5. Triple-Slash Directives
Ви іноді бачите такі коментарі:
/// <reference path="..." />
/// <reference types="node" />
Це XML-подібні директиви для компілятора.
path: каже "включи цей файл у компіляцію". Потрібно тільки якщо ви не використовуєте модулі (дуже старий проект).types: каже "завантаж типи для цієї бібліотеки" (наприклад,node,jest). Зазвичай це робиться черезtsconfig("types": ["node"]), але іноді корисно в окремому файлі.
6. Ambient Context: declare global
Якщо ви знаходитесь всередині модуля (є import/export), але хочете додати щось глобально.
export {} // Робимо файл модулем
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION__: any
}
}
// Тепер це працює всюди
// window.__REDUX_DEVTOOLS_EXTENSION__
7. Error Handling Patterns
TypeScript дозволяє робити помилки безпечними.
unknown в catch
За замовчуванням error в catch блоці — any. Це погано.
Вмикайте "useUnknownInCatchVariables": true в tsconfig.
try {
// ...
} catch (e: unknown) {
// Тепер ми повинні перевірити тип
if (e instanceof Error) {
console.error(e.message)
} else {
console.error('Unknown error', e)
}
}
Custom Error Classes
class HttpError extends Error {
constructor(
public status: number,
message: string,
) {
super(message)
this.name = 'HttpError' // Важливо для логування
}
}
function fetchUser() {
throw new HttpError(404, 'User not found')
}
Result Pattern (Functional)
Замість throw, повертайте об'єкт результату (як в Rust або Go).
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return { ok: false, error: 'Division by zero' }
return { ok: true, value: a / b }
}
const res = divide(10, 0)
if (res.ok) {
console.log(res.value)
} else {
console.log(res.error)
}
Цей патерн примушує обробляти помилки.
8. Екосистема: Zod (Validation)
TypeScript перевіряє типи під час компіляції. Zod перевіряє їх під час виконання (Runtime). Це ідеальна пара.
import { z } from 'zod'
// 1. Створюємо схему
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
website: z.string().url().optional(),
})
// 2. Отримуємо TS тип автоматично!
type User = z.infer<typeof UserSchema>
// 3. Валідуємо дані (з API)
function saveUser(input: unknown) {
const result = UserSchema.safeParse(input)
if (!result.success) {
console.error('Validation failed:', result.error)
return
}
// Тут input вже типізований як User
const user = result.data
console.log(user.email)
}
Це стандарт де-факто для валідації форм та API в сучасному TS.
9. Продуктивність Компіляції
Коли проект росте, tsc стає повільним.
Як прискорити?
- Використовуйте
incremental: true. TS буде кешувати результати попередньої збірки. skipLibCheck: true. Не перевіряйте типи вnode_modules. Довіряйте авторам бібліотек.- Уникайте великих Union Types.
// Це повільно, якщо іконок 1000+ type IconName = "icon-1" | "icon-2" | ...; - Діагностика. Запустіть
tsc --extendedDiagnostics, щоб побачити, скільки часу йде на що.
10. tRPC (TypeScript RPC)
Це магія. Якщо у вас TS на бекенді і TS на фронтенді, ви можете імпортувати типи бекенду прямо на фронтенд.
Жодних генераторів, жодних swagger.yaml.
// Backend
const appRouter = router({
getUser: publicProcedure.input(z.string()).query((req) => {
return { id: req.input, name: 'Alice' }
}),
})
export type AppRouter = typeof appRouter
// Frontend
const user = await trpc.getUser.query('123')
// user.name -> Автодоповнення працює!
// Якщо змінити ім'я на бекенді, фронтенд впаде при компіляції.
Це майбутнє веб-розробки.
11. Final Exam: Mini-Library Build
Ваше завдання — створити міні-бібліотеку для роботи з localStorage, яка буде повністю типізована.
Вимоги
- Клас
TypedStorage<Schema>. - Схема передається як Generic (ключ -> тип).
- Методи
getItem,setItemповинні приймати тільки валідні ключі і повертати правильні типи. - Підтримка серіалізації (JSON.stringify/parse) автоматично.
Приклад
interface MyStorage {
theme: 'dark' | 'light'
volume: number
}
const store = new TypedStorage<MyStorage>()
store.setItem('theme', 'dark') // ✅
store.setItem('volume', 50) // ✅
// store.setItem("theme", 123); // 🛑 Error
const t = store.getItem('theme') // Type: "dark" | "light" | null
12. Шпаргалка (Cheatsheet)
| Налаштування / Поняття | Опис |
|---|---|
strict: true | Must have. Вмикає суворі перевірки. |
skipLibCheck: true | Прискорює збірку, ігноруючи node_modules. |
.d.ts | Файли декларацій (тільки типи). |
declare global | Розширення глобального скоупу (Window, Process). |
unknown | Безпечний аналог any. |
zod | Runtime валідація + генерація типів. |
Monorepo | references + composite. |
13. Advanced tsconfig Options
Ми розглянули базу, тепер — просунуті фішки.
paths (Module Resolution)
Магія абсолютних імпортів. Замість ../../../../components/Button пишемо @/components/Button.
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"]
}
}
}
rootDirs (Virtual Directories)
Дозваляє об'єднати декілька папок в одну "віртуальну".
Корисно, якщо ви генеруєте частину коду в src/generated, але хочете імпортувати його як import ... from './generated'.
preserveConstEnums
За замовчуванням const enum видаляється і замінюється на число (інлайн).
Якщо ви пишете бібліотеку, це може бути проблемою для користувачів. Ставте true, щоб зберегти об'єкт enum в JS.
14. Compilation Pipelines: tsc vs The World
У 2024 році ми рідко використовуємо tsc для генерації JS.
tsc: Найповільніший. Робить перевірку типів + транспіляцію.Babel: Вирізає типи (strip types). Дуже швидкий. Але не перевіряє помилки.swc/esbuild: Написані на Rust/Go. В 20-100 разів швидші за Babel.
Ідеальний пайплайн:
- Dev Server (Vite/Next):
swc/esbuild(щоб все літало). - CI / Pre-commit:
tsc --noEmit(тільки перевірка типів). - Build:
swc/esbuild(генерація бандлу).
15. ESM vs CJS Interop (Пекло Модулів)
Найбільший біль в екосистемі Node.js/TS — це імпорт CommonJS модулів в ESM проєкт.
Помилка:
TypeError: React is not a function або Has no default export.
Рішення:
Вмикайте ці прапорці в tsconfig.json:
{
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
Це дозволяє писати import React from 'react' замість import * as React from 'react', навіть якщо бібліотека написана в CommonJS.
16. Performance Optimization
Як писати код, який менше навантажує компілятор і браузер?
const enum
Звичайний enum генерує IIFE об'єкт. const enum просто підставляє значення.
const enum Direction {
Up,
Down,
}
const d = Direction.Up
// JS output:
// const d = 0; // Нуль оверхеду!
import type
Допомагає бандлерам (Webpack) ефективніше видаляти зайвий код (Tree Shaking).
import { type User, createUser } from './user'
// User використовується тільки як тип, тому цей імпорт повністю зникне з JS бандлу,
// якщо createUser не використовується.
17. Case Study: Type-Safe API Client
Давайте створимо обгортку над fetch з Zod.
// api.ts
async function request<T>(url: string, schema: z.ZodType<T>): Promise<T> {
const response = await fetch(url)
const data = await response.json()
// Parse кине помилку, якщо дані не валідні!
return schema.parse(data)
}
// user-service.ts
const UserResponseSchema = z.object({
id: z.number(),
name: z.string(),
company: z.object({
name: z.string(),
}),
})
// Автоматичний вивід типу відповіді
async function getUser(id: number) {
// result має тип { id: number; name: string; ... }
const result = await request(`/users/${id}`, UserResponseSchema)
console.log(result.company.name)
}
Це "залізобетонна" гарантія, що ваш фронтенд не впаде через несподівану зміну API.
18. Global Types Augmentation: process.env
В Node.js process.env типізований як Dict<string>.
Давайте додамо наші змінні середовища.
// env.d.ts
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string
PORT: string
NODE_ENV: 'development' | 'production'
}
}
Тепер:
process.env.DATABASE_URL // string
process.env.PORT // string
// process.env.UNKNOWN // 🛑 Error
19. Final Challenge: Fully Typed Redux Clone
Ми робили фрагменти. Тепер повна картина архітектури.
type Action<T = any> = { type: T }
type Reducer<S, A extends Action> = (state: S, action: A) => S
type Listener = () => void
class Store<S, A extends Action> {
private state: S
private listeners: Listener[] = []
constructor(
private reducer: Reducer<S, A>,
initialState: S,
) {
this.state = initialState
}
getState(): S {
return this.state
}
dispatch(action: A): void {
this.state = this.reducer(this.state, action)
this.listeners.forEach((l) => l())
}
subscribe(listener: Listener): () => void {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter((l) => l !== listener)
}
}
}
Завдання:
Додайте підтримку Variable Arguments в dispatch якщо action creator приймає аргументи. (Підказка: Parameters<T>).
20. Шпаргалка (Cheatsheet)
| Налаштування / Поняття | Опис |
|---|---|
strict: true | Must have. Вмикає суворі перевірки. |
skipLibCheck: true | Прискорює збірку, ігноруючи node_modules. |
.d.ts | Файли декларацій (тільки типи). |
declare global | Розширення глобального скоупу (Window, Process). |
unknown | Безпечний аналог any. |
zod | Runtime валідація + генерація типів. |
Monorepo | references + composite. |
paths | Аліаси шляхів (@/components). |
esModuleInterop | Імпорт CJS пакетів як ESM. |
Фінал
Ви пройшли шлях від console.log до архітектури складних систем.
TypeScript — це інвестиція. Спочатку ви витрачаєте час на написання типів, але потім вони економлять вам години на дебаггингу та рефакторингу.
Що далі?
- Читайте вихідний код популярних бібліотек (RxJS, TypeORM, Zod). Це найкращий підручник.
- Спробуйте написати свої типи для нетипізованої бібліотеки (
DefinitelyTyped). - Почніть використовувати TS в кожному новому проекті.
Успіхів у підкоренні типів!
Курс завершено!