Уявіть ситуацію. П'ятниця, 17:00. Ви заливаєте хотфікс на продакшн. Все протестовано локально. Менеджер киває. Ви натискаєте "Deploy". Через 5 хвилин у Slack починається пекло. Користувачі скаржаться, що сторінка оплати "біла". В консолі браузера червоним світиться легендарна помилка:
Uncaught TypeError: Cannot read properties of undefined (reading 'price')Ви відкриваєте код і бачите це:
function calculateTotal(cart) {
return cart.items.reduce((sum, item) => sum + item.price, 0)
}
Виявляється, в одному з випадків cart прийшов не як об'єкт, а як null. Або item не мав поля price. JavaScript "проковтнув" це на етапі написання, але "вибухнув" на етапі виконання (Runtime).
JavaScript — це динамічно типізована мова. Це означає, що ви можете покласти кота в коробку з написом "собака", і JS скаже "Окей", поки ви не спробуєте змусити цього кота гавкати.
"TypeScript doesn't prevent bugs. It prevents you from writing code that looks like it works but inevitably fails."
TypeScript — це статичний аналізатор і надбудова над JavaScript. Він змушує вас чітко сказати: "В цій змінній має бути число, і нічого більше". Якщо ви спробуєте запхнути туди рядок — код навіть не скомпілюється. Ви дізнаєтесь про помилку в редакторі, а не від розлюченого клієнта.
JavaScript створювався за 10 днів у 1995 році для простих скриптів (дзвонити дзвіночком, коли натискаєш кнопку). Ніхто не думав, що на ньому будуть писати:
Коли проєкти розрослися до мільйонів рядків, підтримувати "динамічний хаос" стало неможливо. Google намагався зробити Dart. Facebook — Flow. Але переміг Microsoft з TypeScript (реліз у 2012), тому що:
Перш ніж писати код, давайте зрозуміємо, як TS працює. Браузери НЕ РОЗУМІЮТЬ TypeScript. Node.js (без спеціальних прапорів) НЕ РОЗУМІЄ TypeScript.
TypeScript — це інструмент розробки. Він існує тільки до моменту "компіляції" (транспіляції).
.ts файл.tsc) перевіряє типи.
.js файлу..js файл. В цьому файлі типів вже немає. Вони "розчинилися" (erased).tsc (TypeScript Compiler)Давайте зробимо це "руками", без магії Vite, щоб відчути різницю.
Встановіть TypeScript глобально (або використовуйте npx):
npm install -g typescript
tsc --version
Створіть файл index.ts:
const greeting: string = 'Hello, World!'
console.log(greeting)
Тепер скомпілюйте його:
tsc index.ts
Ви побачите новий файл index.js. За замовчуванням TS компілює в старий стандарт ES3 (щоб працювало навіть в IE6):
// index.js (Output)
var greeting = 'Hello, World!'
console.log(greeting)
Бачите? const перетворився на var, а тип : string зник.
Спробуйте змінити стандарт.
tsc index.ts --target ES2020
Тепер index.js залишить const, бо ES2020 це підтримує.
tsconfig.jsonЩоб не писати довгі аргументи в консолі, ми використовуємо tsconfig.json. Це "пульт керування" проектом.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"noEmit": true // <-- Про це нижче
}
}
| Опція | Значення | Опис |
|---|---|---|
target | ES5, ES2020, ESNext | У яку версію JS компілювати код. Якщо треба підтримати старі браузери, ставимо ES5. |
module | CommonJS, ESNext, NodeNext | Яку систему модулів використовувати. Для сучасних фронтенд-проектів ставимо ESNext. |
lib | ["DOM", "ESNext"] | Які глобальні типи доступні (наприклад, document, window, Array.flat). |
strict | true | Вмикає "Суворий режим". Обов'язково для всіх нових проектів. |
noImplicitAny | true | Забороняє змінні без типу, які автоматично стають any. Не дає лінитися. |
strictNullChecks | true | Робить null та undefined окремими типами. Рятує від "Cannot read property of undefined". |
outDir | ./dist | Куди складати скомпільовані .js файли. |
rootDir | ./src | Де лежить ваш вихідний код. |
noEmit: true?)У реальному сучасному проекті ми рідко використовуємо tsc для генерації файлів.
Чому? Тому що tsc повільний як збирач (bundler).
Ми використовуємо Vite.
esbuild (написаний на Go), який в 10-100 разів швидший за tsc для транспіляції файлів.tsc) залишається тільки як поліцейський (Type Checker).Ось чому в tsconfig.json для Vite проектів ви побачите:
"noEmit": true
Це означає: "TypeScript, будь ласка, тільки перевіряй помилки і не створюй жодних .js файлів. Цим займеться Vite."
tsserver) підсвічує помилки в реальному часі.npm run build, запускається скрипт:
tsc -b && vite build
tsc перевіряє типи (-b - build mode). Якщо є помилки, білд падає. Якщо все ОК, Vite збирає бандл.tsconfig.json для ViteКоли ви створюєте проєкт через Vite, ви бачите трохи інший конфіг. Давайте розберемо його.
tsconfig.app.json){
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true, // Важливо для esbuild, який компілює файли окремо
"noEmit": true, // Vite збирає, TS перевіряє
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Якщо ви хочете максимальної безпеки і комфорту, ось розширена версія, яку використовують у великих ентерпрайз проєктах.
{
"compilerOptions": {
/* Base Options */
"target": "ES2022", // Сучасні браузери (підтримка Top-level await)
"module": "ESNext",
"moduleResolution": "bundler", // Найкращий варіант для Vite/Webpack 5
"lib": ["ESNext", "DOM", "DOM.Iterable"],
/* Strict Type-Checking (The Armor) */
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true, // catch(e) -> e is unknown (not any)
/* Code Quality (The Linting) */
"noUnusedLocals": true, // Невикористані змінні - це сміття
"noUnusedParameters": true, // Невикористані аргументи - це помилка
"noImplicitReturns": true, // Функція маж повертати значення у всіх гілках
"noFallthroughCasesInSwitch": true, // Захист від забутого break
"forceConsistentCasingInFileNames": true, // Windows/Linux сумісність (File.ts != file.ts)
/* Bundler Integration */
"noEmit": true, // Віддаємо збірку Vite
"allowImportingTsExtensions": true, // Дозволяє import X from './file.ts'
"resolveJsonModule": true, // import config from './config.json'
"isolatedModules": true, // Захист від features, які не підтримує esbuild (наприклад const enum)
"esModuleInterop": true, // Дозволяє import React from 'react' (CommonJS сумісність)
"skipLibCheck": true, // Прискорює збірку, ігноруючи типи в node_modules (ми їх не правимо)
},
"include": ["src/**/*"],
}
Цей конфіг не дозволить вам написати "брудний" код. Спочатку буде боляче, але потім він врятує вам години дебаггингу.
Перш ніж ми пірнемо в типи даних, давайте розберемося з граматикою. У TypeScript типи пишуться після назви змінної через двокрапку. Це називається Type Annotation.
// Змінна
let variableName: Type = value
// Функція
function functionName(param: Type): ReturnType {
// ...
}
let age: number = 25
let userName: string = 'Alice'
let isActive: boolean = true
function multiply(a: number, b: number): number {
return a * b
}
typeofУ JavaScript (і TypeScript) є оператор typeof, який повертає тип значення у вигляді рядка.
У TypeScript він стає ще потужнішим, бо дозволяє нам робити "Type Guards" (Захисники Типу).
console.log(typeof 'Hello') // "string"
console.log(typeof 42) // "number"
console.log(typeof true) // "boolean"
console.log(typeof {}) // "object"
console.log(typeof undefined) // "undefined"
console.log(typeof null) // "object" (Це відомий баг JS, з яким ми живемо)
Ми будемо часто використовувати typeof для перевірок:
function printId(id: number | string) {
if (typeof id === 'string') {
// Тут TS знає, що id - це string
console.log(id.toUpperCase())
} else {
// А тут - що це number
console.log(id.toFixed(2))
}
}
Функції — це основний будівельний блок будь-якого застосунку. У TypeScript ми типізуємо не тільки те, що функція приймає, але й те, що вона повертає.
// (a: number, b: number) -> це параметри
// : number -> це тип значення, яке повертається
function sum(a: number, b: number): number {
return a + b
}
Синтаксис трохи відрізняється, але суть та сама.
const multiply = (a: number, b: number): number => {
return a * b
}
// Скорочений запис (Implicit return)
const divide = (a: number, b: number): number => a / b
Іноді аргумент можна не передавати. Ми позначаємо це знаком питання ?.
function greet(name: string, title?: string): string {
// title має тип "string | undefined"
if (title) {
return `Hello, ${title} ${name}`
}
return `Hello, ${name}`
}
greet('Alice') // ✅
greet('Bob', 'Mr.') // ✅
Якщо значення не передано, використовується дефолтне.
// role автоматично стає string. Знак ? не потрібен.
function createUser(name: string, role: string = 'guest') {
console.log(`User ${name} has role ${role}`)
}
createUser('Alice') // role: "guest"
createUser('Bob', 'admin') // role: "admin"
Для функцій, які приймають довільну кількість аргументів.
// ...numbers збирає всі аргументи в масив чисел
function total(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0)
}
total(10, 20, 30) // 60
Іноді нам треба передати функцію як аргумент (Callback). Як описати її тип?
// Тип: функція, яка приймає рядок і нічого не повертає
type LogCallback = (message: string) => void
function processData(data: string, onComplete: LogCallback) {
// ... logic
onComplete('Process finished')
}
processData('file.txt', (msg) => console.log(msg))
У JavaScript типи "м'які". У TypeScript вони "тверді", але розумні. Давайте розглянемо базові будівельні блоки, але з точки зору продакшн-коду.
Це не просто текст. Це ідентифікатори, повідомлення, ключі.
// ❌ Погано: Неявне визначення (хоча TS це розуміє - Type Inference)
const userName: string = 'Oleksandr'
// ✅ Добре: TS сам розуміє, що це string (Inference)
const userName = 'Oleksandr'
// 🛑 ПОМИЛКА
userName = 42 // Error: Type 'number' is not assignable to type 'string'.
Але що робити з рядками, які використовуються для форматування? Template Literal Types (ми розглянемо їх глибше пізніше, але ось анонс).
const currency = 'UAH'
const amount = 100
// Тип message автоматично стає string
const message = `Total: ${amount} ${currency}`
У JS (і TS) всі числа — це числа з плаваючою крапкою (IEEE 754). Це джерело нескінченного болю у фінансах.
const price: number = 0.1 + 0.2 // 0.30000000000000004
number для зберігання грошей, якщо ви робите математичні операції на фронтенді.
Використовуйте цілі числа (копійки замість гривень) або спеціальні бібліотеки (decimal.js).
А ще краще — новий тип bigint для великих цілих чисел.Для дуже великих чисел (більше $2^{53} - 1$). Це важливо для Id з бази даних, криптографії та точних фінансів.
// Потрібно target: ES2020+
const reallyBigNumber: bigint = 9007199254740991n // суфікс 'n'
Це одна з найпотужніших фіч TS. Часто змінна може бути не одного типу, а кількох.
Ми використовуємо символ | (pipe), щоб сказати "АБО".
let id: number | string
id = 101 // ✅
id = 'user_101' // ✅
// id = true; // 🛑 Error
Якщо ви маєте string | number, ви можете використовувати лише ті методи, які є спільними для обох типів (наприклад toString()).
function printId(id: number | string) {
// console.log(id.toUpperCase()); // 🛑 Помилка! У number немає toUpperCase().
console.log(id.toString()) // ✅ ОК, бо це є і там, і там.
}
Щоб працювати зі специфічними методами, нам треба "звузити" тип. Тут нам допомагає typeof, який ми вчили раніше.
function formatPrice(price: number | string) {
if (typeof price === 'string') {
// У цьому блоці price — це string
return parseFloat(price).toFixed(2)
} else {
// У цьому блоці price — це number
return price.toFixed(2)
}
}
Ми можемо об'єднувати не просто типи, а конкретні значення.
// Тільки ці три рядки дозволені
type WindowState = 'open' | 'closed' | 'minimized'
let state: WindowState = 'open'
// state = "broken"; // 🛑 Error
Це часто замінює enum (перелічення), бо простіше і працює швидше.
Ми розглянули string, number, boolean. Але диявол криється в деталях.
Перш ніж йти далі, розберемо ключове слово type.
Часто типи стають довгими і складними. Щоб не писати їх усюди, ми даємо їм ім'я.
// Створюємо псевдонім для типу string
type Name = string
// Створюємо псевдонім для складнішого типу (Union)
type ID = string | number
let userId: ID = 123
userId = 'user_456'
Це як змінна, але для типів. Вона зникає при компіляції.
TypeScript дозволяє робити магію з рядками. Ви можете обмежити рядок шаблоном.
type Protocol = 'http' | 'https'
type Domain = 'kostyl.dev' | 'google.com'
type URL = `${Protocol}://${Domain}`
const mySite: URL = 'https://kostyl.dev' // ✅
// const wrong: URL = "ftp://kostyl.dev"; // ❌ Error
Це надзвичайно потужно для типізації API ендпоінтів, CSS класів (наприклад, Tailwind) або форматів дат.
Тип symbol використовується для створення унікальних ключів об'єктів. Це рідко потрібно в бізнес-логіці, але часто в бібліотеках.
const key1: symbol = Symbol('key')
const key2: symbol = Symbol('key')
// console.log(key1 === key2); // false! Навіть якщо опис однаковий.
Чому 0.1 + 0.2 !== 0.3? Тому що комп'ютери зберігають числа у двійковій системі.
Якщо ви робите фінансовий додаток, number — це ризик.
// Проблема number
const price = 19.99
const quantity = 3
const total = price * quantity // 59.96999999999999 😱
// Рішення BigInt (працюємо в копійках)
const priceBig = 1999n // 19.99 грн -> 1999 копійок
const qtyBig = 3n
const totalBig = priceBig * qtyBig // 5997n -> 59.97 грн. Ідеально точно.
BigInt не можна змішувати з number.
10n + 10 видасть помилку. Вам потрібно явно перетворити типи: 10n + BigInt(10).Істина чи брехня. Усе просто, так?
Hе зовсім. У JS є концепція truthy та falsy значень. TS допомагає не вистрілити собі в ногу.
const isError: boolean = false
if (isError) {
// Цей код ніколи не виконається (Dead Code)
// TS може підсвітити це, якщо налаштований правильно
console.error('Boom!')
}
Тут починається найцікавіше. Початківці часто плутають ці поняття.
undefined: Значення ще не було присвоєно. Змінна оголошена, але пуста. Це "ненавмисна" відсутність.null: Значення відсутнє навмисно. Ми явно сказали "тут нічого немає".interface UserProfile {
id: string
bio: string | null // Користувач може стерти біографію
website?: string // Користувач може навіть не заповнювати це поле (буде undefined)
}
function printBio(user: UserProfile) {
if (user.bio === null) {
console.log('No bio provided')
return
}
// Тут TS знає, що user.bio - це точно string!
console.log(user.bio.toUpperCase())
}
strictNullChecks рятує життя?
Якби це налаштування було вимкнене, ми могли б викликати toUpperCase() на null, і TS промовчав би. У Runtime ми б отримали краш.
Зі strictNullChecks, TS змушує нас перевірити на null перед використанням.Використовується виключно для функцій, які нічого не повертають.
// Аналіз коду:
// 1. Функція логує повідомлення (Side effect).
// 2. Вона не має return.
// 3. Її результат - void (хоча технічно в JS це undefined).
function logError(message: string): void {
console.error(`[SYSTEM ERROR]: ${message}`)
}
const result = logError('DB Timeout') // Тип result - void. З ним нічого не можна зробити.
Тип any вимикає перевірку типів для змінної. Це "Escape Hatch".
Використовувати any — це як їздити без паска безпеки на швидкості 200 км/год.
// ❌ НІКОЛИ ТАК НЕ РОБІТЬ (хіба що під час міграції старого JS коду)
let data: any = 'Hello'
data = 42 // ОК
data.methodHuh() // ОК в редакторі -> Crash в браузері 💥
TypeScript дозволяє вам це зробити, але ви втрачаєте весь сенс використання TS.
Якщо ви бачите any в коді — це Technical Debt.
Якщо ви дійсно не знаєте, що прийде (наприклад, відповідь від стороннього API або парсинг JSON), використовуйте unknown.
Він каже: "Я не знаю, що це, тому не дозволю тобі це використовувати, поки ти не перевіриш тип".
function safeParse(json: string): unknown {
return JSON.parse(json)
}
const result = safeParse('{"name": "Alice"}')
// 🛑 Error: Object is of type 'unknown'.
// console.log(result.name);
// Ми не можемо просто звернутися до поля, бо не знаємо, чи це об'єкт.
// ✅ Треба зробити звуження типу (Type Narrowing)
if (typeof result === 'object' && result !== null && 'name' in result) {
// Тепер це безпечно, але TS все ще обережний.
// Для реальних задач краще використовувати Zod/Yup або Type Guards (про це пізніше).
console.log((result as any).name) // Все ще брудно, але це ілюстрація обмеження
}
Тип never означає, що значення ніколи не може статися.
Це звучить дивно, але це супер-корисно для:
// Сценарій 1: Функція-бомба
function throwError(msg: string): never {
throw new Error(msg)
}
// Сценарій 2: Вичерпна перевірка
type Status = 'success' | 'error' | 'loading'
function getMessage(s: Status): string {
switch (s) {
case 'success':
return 'Done!'
case 'error':
return 'Oops!'
case 'loading':
return 'Loading...'
default:
// Якщо ми додамо новий статус в type Status, але забудемо тут case,
// TS видасть помилку тут, бо s не буде never.
const _exhaustiveCheck: never = s
return _exhaustiveCheck
}
}
Список друзів, товари в кошику, історія повідомлень — усе це масиви.
Є два синтаксиси:
const scores: number[] = [1, 2, 3] // Більш поширений
const names: Array<string> = ['Alice', 'Bob'] // Дженерик синтаксис (стара школа)
А що, як масив такий важливий, що ми не хочемо, щоб його змінювали? Immutability (незмінність) — це ключ до стабільного React коду.
// ReadonlyArray
const sensors: readonly number[] = [22.5, 23.1, 22.8]
// 🛑 Error: Property 'push' does not exist on type 'readonly number[]'.
// sensors.push(24.0);
// Ми можемо тільки читати або створювати нові масиви на основі цього
const newSensors = [...sensors, 24.0]
Іноді нам потрібен масив фіксованої довжини з різними типами на конкретних позиціях.
Найкласичніший приклад — React useState.
// Це Tuple: [string, number]
// Перший елемент ЗАВЖДИ рядок, Другий ЗАВЖДИ число.
type UserRecord = [string, number]
const record: UserRecord = ['Alice', 1]
// Деструктуризація працює ідеально
const [username, id] = record
Якщо вийди за межі довжини або переплутати типи — буде помилка.
.push() у звичайний Tuple (це давня особливість/баг дизайну TS).
Але при доступі за індексом, якого не має бути, він сваритиметься.
Щоб зробити Tuple справді фіксованим, додайте readonly.const point: readonly [number, number] = [10, 20]
// point.push(30); // Тепер це помилка!
Давайте зберемо все разом. Ми отримуємо дані про користувача з бекенду. Ось як виглядав би JS код:
async function getUser(id) {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
// Хто знає, що там повертається? Ніхто.
Ось TypeScript підхід (Рівень 1: Базові типи):
// 1. Визначаємо форму даних (Shape)
// Поки використовуємо type, про interface поговоримо пізніше
type Address = {
city: string
zip: number // краще string для zip, але для прикладу number
street?: string // Необов'язкове поле
}
type UserResponse = {
id: number
username: string
isActive: boolean
tags: string[] // Масив рядків
address: Address // Вкладений об'єкт
lastLogin: string | null // Може бути null
}
// 2. Типізуємо функцію
async function getUser(id: number): Promise<UserResponse> {
const res = await fetch(`/api/users/${id}`)
// ПРИМІТКА: fetch не кидає помилку на 404/500, треба перевіряти ok
if (!res.ok) {
throw new Error('Network response was not ok')
}
// cast result as UserResponse
// (Увага: це "довіра" розробника, TS не може перевірити рантайм відповідь тут)
return res.json() as Promise<UserResponse>
}
// 3. Використання
async function main() {
try {
const user = await getUser(101)
console.log(`Hello, ${user.username.toUpperCase()}`)
if (user.lastLogin) {
console.log(`Last login: ${user.lastLogin}`)
}
// Автодоповнення працює! user.address.city
} catch (e) {
console.error(e)
}
}
Це лише верхівка айсберга. Ми типізували "щасливий шлях". Але що таке Promise? Чому as Promise<UserResponse> це трохи небезпечно? Про це далі.
TypeScript розумний. Йому не завжди потрібно казати очевидне.
// TS бачить: це рядок. Йому не треба писати ': string'
let framework = 'React'
// TS бачить: це число
let version = 18
// TS "виводить" тип результату функції
// add має тип (a: number, b: number) => number
function add(a: number, b: number) {
return a + b
}
const x = 10 краще, ніж const x: number = 10.// ✅ Explicit return type
// Якщо ми випадково повернемо рядок, TS одразу це підсвітить ТУТ.
function getUserName(user: User): string {
return user.firstName + ' ' + user.lastName
}
TS розуміє контекст.
const buttons = ['Save', 'Cancel', 'Delete']
// TS знає, що 'btn' - це string, тому що масив рядків.
// Вам не треба писати (btn: string)
buttons.forEach((btn) => {
console.log(btn.toUpperCase()) // ✅ Метод string доступний
})
Іноді ви знаєте про тип більше, ніж TypeScript.
Наприклад, ви отримуєте елемент з DOM. TS знає, що це HTMLElement або null. Ви знаєте, що це HTMLInputElement, бо ви його туди поклали.
as// 1. Отримуємо елемент (він може бути null, і TS думає, що це просто HTMLElement)
const input = document.getElementById('user-email')
// 2. Ми кажемо: "Повір мені, це HTMLInputElement"
const emailInput = input as HTMLInputElement
// Тепер доступні специфічні методи
console.log(emailInput.value)
asas — це спосіб вимкнути перевірку. Ви берете відповідальність на себе.
const safeData = 'Hello' as number // 🛑 TS Error: Conversion of type 'string' to type 'number' may be a mistake...
// АЛЕ через unknown можна зламати все
const dangerousData = 'Hello' as unknown as number // ✅ TS мовчить
// dangerousData.toFixed(2); // 💥 Runtime Crash!
Використовуйте as тільки тоді, коли ви на 100% впевнені.
Кращий шлях — Type Guards (про це в розділі 5).
as const)Це супер-фіча для Redux actions, конфігів та незмінних об'єктів. Вона робить дві речі:
readonly.string, number).// Без as const
const config = {
endpoint: 'https://api.example.com',
retries: 3,
}
// Тип config: { endpoint: string; retries: number; }
// config.endpoint = "oops"; // Дозволено (хоча це може зламати app)
// З as const
const strictConfig = {
endpoint: 'https://api.example.com',
retries: 3,
} as const
// Тип strictConfig:
// {
// readonly endpoint: "https://api.example.com"; // Тип не string, а саме цей рядок!
// readonly retries: 3; // Тип не number, а саме 3!
// }
// strictConfig.retries = 5; // 🛑 Error: Read-only!
Це критично важливо, коли значення саме по собі є типом.
declare)Що робити, якщо ви використовуєте бібліотеку JS, яка не має типів (наприклад, старий jQuery плагін або змінна, яку сервер вставляє в window)?
TS буде кричати: "Я не знаю, що таке MY_GLOBAL_VAR!".
Ви можете використати declare, щоб сказати: "Ця змінна існує, повір мені".
// global.d.ts або прямо у файлі
declare const MY_ANALYTICS_ID: string
function track() {
console.log(MY_ANALYTICS_ID) // Тепер помилки немає
}
Це часто використовується для типізації window.
// Розширення інтерфейсу Window
declare global {
interface Window {
__REDUX_DEVTOOLS_EXTENSION__: any
}
}
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ // ✅
Давайте закріпимо знання про void, unknown та never, створивши утиліту для логування.
// Рівень логування (Union Type)
type LogLevel = 'info' | 'warn' | 'error'
interface LogConfig {
debugMode: boolean
appVersion: string
}
// Функція, яка форматує повідомлення (повертає string)
function formatMessage(level: LogLevel, msg: string, config: LogConfig): string {
const timestamp = new Date().toISOString()
return `[${timestamp}] [${level.toUpperCase()}] (v${config.appVersion}): ${msg}`
}
// Функція, яка відправляє лог (повертає void)
function transport(formattedMsg: string): void {
console.log(formattedMsg)
// Тут міг би бути sendToSentry(formattedMsg);
}
// Головна функція (параметри явно типізовані)
function log(level: LogLevel, message: unknown, config: LogConfig): void {
if (!config.debugMode && level === 'info') {
return // Early return
}
// Безпечна обробка unknown
let safeMessage: string
if (typeof message === 'string') {
safeMessage = message
} else if (message instanceof Error) {
safeMessage = message.message
} else {
// Fallback для об'єктів
try {
safeMessage = JSON.stringify(message)
} catch {
safeMessage = 'Unknown Object'
}
}
const output = formatMessage(level, safeMessage, config)
transport(output)
}
// Тест
const appConfig = { debugMode: true, appVersion: '1.0.0' } as const
log('error', new Error('DB Connection Failed'), appConfig)
log('warn', { userId: 123, action: 'drop_table' }, appConfig)
log('info', 'App started', appConfig)
// log("fatal", "Boom", appConfig); // 🛑 Error: Argument of type '"fatal"' is not assignable to parameter of type 'LogLevel'.
Цей приклад ілюструє:
LogLevel) для обмеження варіантів.message) для безпечного прийому будь-яких даних.typeof, instanceof) для перетворення unknown -> string.Навіть досвідчені розробники роблять ці помилки. Давайте розберемо їх, щоб ви не наступали на ті ж граблі.
Number, String, Boolean (З великої літери)У JS є примітиви (string) і об'єкти-обгортки (String).
У TS завжди використовуйте примітиви (з малої літери).
// ❌ НЕПРАВИЛЬНО
function reverse(str: String): String {
return new String(str).split('').reverse().join('')
}
// ✅ ПРАВИЛЬНО
function reverse(str: string): string {
return str.split('').reverse().join('')
}
Тип String — це об'єкт. Він масивніший і веде себе інакше при порівнянні.
Function і ObjectTypeScript має глобальні типи Function і Object. Вони майже марні і небезпечні.
Function: Приймає будь-які аргументи і повертає any. Це діра в безпеці.Object: Це будь-яке значення, яке не є nil (тобто не null/undefined). Це включає масиви, функції і, власне, об'єкти.// ❌ Погано
function doSomething(cb: Function) {
cb(1, 2, 3) // Ніякої перевірки аргументів
}
// ✅ Добре
function doSomething(cb: (a: number, b: number) => void) {
// ...
}
any при міграціїКоли люди переносять JS проект на TS, вони часто ставлять any всюди, "щоб просто запрацювало".
Це називається AnyScript.
Краща стратегія:
allowJs: true в tsconfig.unknown і пишіть коментар // TODO: Fix type.Ви можете запитати: "Навіщо мені це вчити, якщо я можу писати на JS?". Відповідь проста: Інструменти.
Сучасний веб тримається на трьох китах, які існують завдяки TS:
TS перевіряє типи під час написання. Але що, якщо API пришле не те, що ми чекали? Zod дозволяє описати схему, яка працює і як валідатор, і як джерело типів.
import { z } from 'zod'
// Схема
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
})
// Автоматичний вивід типу (Don't Repeat Yourself)
type User = z.infer<typeof UserSchema>
// Використання
const result = UserSchema.safeParse(apiResponse)
if (!result.success) {
console.error(result.error)
}
Якщо у вас TS на бекенді і TS на фронтенді, навіщо вам ручні типи API? tRPC дозволяє викликати функції бекенду на клієнті так, ніби вони локальні, з повним автодоповненням.
Забудьте про SQL помилки в рядках. Ці ORM генерують типи на основі вашої БД.
// Якщо ви змінили ім'я колонки в БД, цей код підсвітиться червоним!
const user = await db.user.findFirst({
where: { email: 'test@example.com' },
})
Розуміння історії допомагає зрозуміти "чому все так".
Microsoft (Андерс Хейлсберг, творець C#) випускає TypeScript 0.8. Спільнота JS налаштована скептично: "Microsoft знову хоче захопити веб?".
Google приймає доленосне рішення: повністю переписати Angular на TypeScript. Це дало TS величезний поштовх.
З'являється DefinitelyTyped і пакет @types/....
Тепер не треба писати файли декларацій вручну для кожної бібліотеки.
Babel додає підтримку парсингу TS. Тепер не обов'язково використовувати компілятор MS для збірки. Це відкрило двері для інтеграції у Create React App.
Сьогодні важко знайти вакансію React/Node.js розробника без вимоги TypeScript.
Ми багато говорили про теорію. Час написати код.
Ваше завдання — створити безпечну обгортку над localStorage.
storage.ts.saveItem(key: string, value: unknown): void.
QuotaExceededError).readItem(key: string): unknown.
null, якщо ключа немає.type Result = { status: 'success'; value: unknown } | { status: 'error'; error: Error }
Ініціалізуйте проєкт (якщо ще ні):
npm create vite@latest storage-app -- --template vanilla-ts
Спочатку опишіть типи функцій (Signatures). Не пишіть реалізацію одразу. Думайте про те, що функція приймає і що повертає.
JSON.parse може впасти. localStorage.setItem може впасти (якщо пам'ять закінчилась).
Використовуйте try-catch.
Спробуйте зберегти:
BigInt (Увага! JSON.stringify впаде на BigInt. Як це виправити? Додайте перевірку на BigInt і киньте помилку, або конвертуйте в рядок).Збережіть це собі.
| Тип | JS Еквівалент | Приклад | Опис |
|---|---|---|---|
string | Pядок | "Hello" | Текст. |
number | Число | 42, 3.14 | Числа (float). Обережно з грошима. |
boolean | Булеве | true | Істина/Брехня. |
text[] | Масив | ["a", "b"] | Список однотипних елементів. |
[str, num] | Масив | ["a", 1] | Tuple (Кортеж). Фіксована довжина і типи. |
any | Усе | ... | НЕ ВИКОРИТОВУВАТИ. Вимикає TS. |
unknown | Усе | ... | Безпечний any. Вимагає перевірки. |
void | undefined | fn() | Функція нічого не повертає. |
never | - | throw | Функція ніколи не завершується успішно. |
object | Об'єкт | {} | Будь-що, що не є примітивом. |
null | null | null | Явна відсутність значення. |
undefined | undefined | undefined | Значення ще не присвоєно. |
tsc --init: Створити tsconfig.json.tsc --noEmit: Перевірити типи без генерації файлів.tsc --watch: Запустити в режимі спостереження.tsc --project tsconfig.prod.json: Використати інший конфіг.npx tset: (Жарт, такої команди немає, але було б круто).Ні. Браузер виконує JavaScript. TypeScript повністю видаляється перед запуском. Єдине сповільнення — це етап збірки (Build time), але з сучасними інструментами (esbuild, swc) це мілісекунди.
Є тренд (наприклад, у Svelte або деяких бібліотеках Google) використовувати JSDoc замість .ts файлів, щоб уникнути кроку компіляції.
Але:
.ts залишається золотим стандартом.Ми заклали фундамент. Ви тепер знаєте не просто "як оголосити змінну", а як налаштовувати компілятор і думати типами.
noEmit, strict, isolatedModules — база сучасного стеку.as — це небезпечно, as const — це корисно.unknown. Це змусить вас написати код обробки, який врятує продакшн.У наступному розділі ми пірнемо в Інтерфейси, Об'єктні типи та магію Union Types, щоб моделювати складні стани UI. Ми навчимося робити так, щоб неправильний стан інтерфейсу був неможливим на рівні коду.
Складні Компоненти: Dialog, Dropdown, Table та Command
Тепер, коли ми освоїли базові компоненти та форми, час перейти до складніших UI patterns. У цій главі ми розберемо компоненти, які роблять інтерфейс по-справжньому інтерактивним та зручним.
Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи
Як описати реальний світ у коді. Interfaces vs Types, Union Types, Discriminated Unions та мистецтво створення неможливих станів.