TypeScript

Продакшн та Екосистема: Advanced Config & Workflow

Як жити з TypeScript у реальному світі. tsconfig, монорепозиторії, Zod, tRPC та оптимізація продуктивності.

Розділ 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 стає повільним. Як прискорити?

  1. Використовуйте incremental: true. TS буде кешувати результати попередньої збірки.
  2. skipLibCheck: true. Не перевіряйте типи в node_modules. Довіряйте авторам бібліотек.
  3. Уникайте великих Union Types.
    // Це повільно, якщо іконок 1000+
    type IconName = "icon-1" | "icon-2" | ...;
    
  4. Діагностика. Запустіть 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, яка буде повністю типізована.

Вимоги

  1. Клас TypedStorage<Schema>.
  2. Схема передається як Generic (ключ -> тип).
  3. Методи getItem, setItem повинні приймати тільки валідні ключі і повертати правильні типи.
  4. Підтримка серіалізації (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: trueMust have. Вмикає суворі перевірки.
skipLibCheck: trueПрискорює збірку, ігноруючи node_modules.
.d.tsФайли декларацій (тільки типи).
declare globalРозширення глобального скоупу (Window, Process).
unknownБезпечний аналог any.
zodRuntime валідація + генерація типів.
Monoreporeferences + composite.

13. Advanced tsconfig Options

Ми розглянули базу, тепер — просунуті фішки.

paths (Module Resolution)

Магія абсолютних імпортів. Замість ../../../../components/Button пишемо @/components/Button.

{
    "compilerOptions": {
        "baseUrl": "./src",
        "paths": {
            "@/*": ["*"],
            "@components/*": ["components/*"]
        }
    }
}
Це налаштування тільки для TS. Вам все одно треба налаштувати Webpack/Vite/Jest, щоб вони розуміли ці аліаси.

rootDirs (Virtual Directories)

Дозваляє об'єднати декілька папок в одну "віртуальну". Корисно, якщо ви генеруєте частину коду в src/generated, але хочете імпортувати його як import ... from './generated'.

preserveConstEnums

За замовчуванням const enum видаляється і замінюється на число (інлайн). Якщо ви пишете бібліотеку, це може бути проблемою для користувачів. Ставте true, щоб зберегти об'єкт enum в JS.


14. Compilation Pipelines: tsc vs The World

У 2024 році ми рідко використовуємо tsc для генерації JS.

  1. tsc: Найповільніший. Робить перевірку типів + транспіляцію.
  2. Babel: Вирізає типи (strip types). Дуже швидкий. Але не перевіряє помилки.
  3. 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: trueMust have. Вмикає суворі перевірки.
skipLibCheck: trueПрискорює збірку, ігноруючи node_modules.
.d.tsФайли декларацій (тільки типи).
declare globalРозширення глобального скоупу (Window, Process).
unknownБезпечний аналог any.
zodRuntime валідація + генерація типів.
Monoreporeferences + composite.
pathsАліаси шляхів (@/components).
esModuleInteropІмпорт CJS пакетів як ESM.

Фінал

Ви пройшли шлях від console.log до архітектури складних систем. TypeScript — це інвестиція. Спочатку ви витрачаєте час на написання типів, але потім вони економлять вам години на дебаггингу та рефакторингу.

Що далі?

  • Читайте вихідний код популярних бібліотек (RxJS, TypeORM, Zod). Це найкращий підручник.
  • Спробуйте написати свої типи для нетипізованої бібліотеки (DefinitelyTyped).
  • Почніть використовувати TS в кожному новому проекті.

Успіхів у підкоренні типів!

Курс завершено!

Вітаю! Ви освоїли TypeScript від А до Я.
Copyright © 2026