Ми вивчили синтаксис. Тепер поговоримо про інфраструктуру. TypeScript — це не тільки мова, це компілятор, і від його налаштування залежить ваше життя (і швидкість CI/CD).
У цьому фінальному розділі ми розберемо все, що потрібно для створення серйозних проектів та бібліотек.
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'.
Це рятує від тисяч багів.
Великі проекти розбивають на частини. Замість одного гігантського 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, а не весь проект. Це прискорює збірку в рази.
Це джерело плутанини.
import/export)Це стандарт ES6. Кожен файл — це модуль. Використовуйте це у 99% випадків.
namespace X { ... })Це старий спосіб TS групувати код у глобальному об'єкті (аналог IIFE).
namespace Validation {
export const emailRegex = /.../
}
// Validation.emailRegex
Коли використовувати Namespaces?
Тільки для написання типів (.d.ts) для старих бібліотек, які використовують глобальні змінні (наприклад, jQuery плагіни).
.d.ts)Це файли, які містять тільки типи, без коду. Вони дозволяють TS розуміти JS код.
declare)Уявіть, що ви підключили бібліотеку super-lib через CDN (глобальна змінна SuperLib).
TS про неї не знає. Дайте йому підказку.
// globals.d.ts
declare const SuperLib: {
init(config: any): void
version: string
}
Ви хочете додати поле до Request в Express.
// express.d.ts
import { User } from './user'
declare module 'express-serve-static-core' {
interface Request {
user?: User // Ми додали це поле!
}
}
Тепер req.user працюватиме у всьому проекті.
Ви іноді бачите такі коментарі:
/// <reference path="..." />
/// <reference types="node" />
Це XML-подібні директиви для компілятора.
path: каже "включи цей файл у компіляцію". Потрібно тільки якщо ви не використовуєте модулі (дуже старий проект).types: каже "завантаж типи для цієї бібліотеки" (наприклад, node, jest). Зазвичай це робиться через tsconfig ("types": ["node"]), але іноді корисно в окремому файлі.declare globalЯкщо ви знаходитесь всередині модуля (є import/export), але хочете додати щось глобально.
export {} // Робимо файл модулем
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION__: any
}
}
// Тепер це працює всюди
// window.__REDUX_DEVTOOLS_EXTENSION__
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)
}
}
class HttpError extends Error {
constructor(
public status: number,
message: string,
) {
super(message)
this.name = 'HttpError' // Важливо для логування
}
}
function fetchUser() {
throw new HttpError(404, 'User not found')
}
Замість 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)
}
Цей патерн примушує обробляти помилки.
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.
Коли проект росте, tsc стає повільним.
Як прискорити?
incremental: true. TS буде кешувати результати попередньої збірки.skipLibCheck: true. Не перевіряйте типи в node_modules. Довіряйте авторам бібліотек.// Це повільно, якщо іконок 1000+
type IconName = "icon-1" | "icon-2" | ...;
tsc --extendedDiagnostics, щоб побачити, скільки часу йде на що.Це магія. Якщо у вас 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 -> Автодоповнення працює!
// Якщо змінити ім'я на бекенді, фронтенд впаде при компіляції.
Це майбутнє веб-розробки.
Ваше завдання — створити міні-бібліотеку для роботи з localStorage, яка буде повністю типізована.
TypedStorage<Schema>.getItem, setItem повинні приймати тільки валідні ключі і повертати правильні типи.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
| Налаштування / Поняття | Опис |
|---|---|
strict: true | Must have. Вмикає суворі перевірки. |
skipLibCheck: true | Прискорює збірку, ігноруючи node_modules. |
.d.ts | Файли декларацій (тільки типи). |
declare global | Розширення глобального скоупу (Window, Process). |
unknown | Безпечний аналог any. |
zod | Runtime валідація + генерація типів. |
Monorepo | references + composite. |
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.
tsc vs The WorldУ 2024 році ми рідко використовуємо tsc для генерації JS.
tsc: Найповільніший. Робить перевірку типів + транспіляцію.Babel: Вирізає типи (strip types). Дуже швидкий. Але не перевіряє помилки.swc / esbuild: Написані на Rust/Go. В 20-100 разів швидші за Babel.Ідеальний пайплайн:
swc/esbuild (щоб все літало).tsc --noEmit (тільки перевірка типів).swc/esbuild (генерація бандлу).Найбільший біль в екосистемі 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.
Як писати код, який менше навантажує компілятор і браузер?
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 не використовується.
Давайте створимо обгортку над 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.
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
Ми робили фрагменти. Тепер повна картина архітектури.
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>).
| Налаштування / Поняття | Опис |
|---|---|
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 — це інвестиція. Спочатку ви витрачаєте час на написання типів, але потім вони економлять вам години на дебаггингу та рефакторингу.
Що далі?
DefinitelyTyped).Успіхів у підкоренні типів!
Курс завершено!