Чому потрібен TypeScript, переваги та недоліки
TypeScript - це надбудова над JavaScript, яка додає статичні типи і виявляє помилки ще до запуску коду.
Теорія
TL;DR
- JavaScript падає в runtime, коли передаєш дані не того типу; TypeScript показує помилку в редакторі ще під час написання
- Аналогія: JavaScript - це виписати чек без перевірки балансу; TypeScript - банківський застосунок, який блокує овердрафт до підтвердження
- TypeScript компілюється в звичайний JavaScript, тому браузери і Node.js запускають його без змін
- Використовуй TS для команд від 5+ людей або проєктів від 10k рядків; для швидких скриптів JS достатньо
Швидкий приклад
// JavaScript: жодного попередження, помилка тільки в runtime
let userAge = 25;
userAge = "25";
console.log(userAge.toFixed(0)); // TypeError: userAge.toFixed is not a function
// TypeScript: редактор показує проблему одразу
let userAge: number = 25;
userAge = "25"; // Помилка: Type 'string' is not assignable to type 'number'
console.log(userAge.toFixed(0)); // Компілятор до цього рядка не дійдеУ JS-версії баг живе непомітно до прод-деплою. TypeScript переміщує цей момент в редактор, до будь-якого шипінгу.
Головна різниця
JavaScript перевіряє типи в момент виконання. TypeScript додає структурну перевірку типів (structural type checking) ще на етапі написання, тому IDE одразу сигналізує про невідповідність. За даними команди TypeScript в Microsoft, це скорочує кількість багів на 15-20% у великих проєктах. Runtime-паніки стають помітками в редакторі.
Коли використовувати
- Прототип до 1k рядків, пишеш сам - JS, накладні витрати на налаштування не виправдані
- Командний проєкт або React/Vue/Next.js застосунок - TS, ловить неправильні props до браузера
- Node.js API з валідацією запитів - TS, перевіряє структуру даних до звернення в базу
- Міграція легасі JS-коду - поступовий перехід з
allowJs: true, без переписування всього підряд - Швидкий скрипт - JS, ітерація швидша
Як працює компілятор
Компілятор TypeScript (tsc) парсить .ts-файли, перевіряє типи за структурою (якщо форми об'єктів збігаються - типи сумісні), потім генерує звичайні .js-файли під ES5 або ES6. У VSCode TypeScript Language Service працює у фоні і дає діагностику в реальному часі без повної компіляції. Типи повністю зникають з вихідного коду. Node.js і браузери їх не бачать.
Типові помилки
Помилка 1: any скрізь
// Неправильно: зводить нанівець весь сенс TypeScript
function processData(data: any) {
return data.foo.bar; // Runtime-краш, як і в чистому JS
}
// Правильно: використовуй unknown і звужуй тип явно
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'foo' in data) {
return (data as { foo: { bar: string } }).foo.bar;
}
}any вимикає всю перевірку типів для цього значення. TypeScript нічим не допомагає. Використовуй unknown замість нього.
Помилка 2: strictNullChecks вимкнений
// Неправильно (strictNullChecks: false у tsconfig)
let user = getUser();
user.name.toUpperCase(); // Падіння, якщо user дорівнює null
// Правильно (strictNullChecks: true)
let user = getUser();
user?.name?.toUpperCase(); // БезпечноБез strictNullChecks TypeScript дозволяє передавати null і undefined куди завгодно. Більшість runtime-помилок лишається прихованою. Вмикай strict-режим з самого початку.
Помилка 3: Не розуміти різниці між interface і type
// interface: підтримує злиття декларацій (declaration merging)
interface User { name: string }
interface User { age: number } // Автоматично об'єднується: { name: string; age: number }
// type: злиття немає, підходить для union і intersection
type Status = 'active' | 'inactive';
type AdminUser = User & { role: 'admin' };Багато команд обирають interface для структур об'єктів і type для union-типів та композицій. Обидва варіанти працюють. Різниця стає відчутною, коли треба розширювати або комбінувати типи.
Де зустрічається в реальному коді
- React / Next.js - типізовані props через
interface Props { user: User } - Express / NestJS - DTO-класи для валідації структури запиту до звернення в базу
- Redux Toolkit -
PayloadAction<User>робить структуру action-ів передбачуваною - Node.js -
@types/nodeнадає типи для стандартної бібліотеки Node
Питання для співбесіди
Q: Що таке structural typing у TypeScript?
A: Два типи сумісні, якщо їхні структури збігаються, незалежно від назви. Об'єкт з { x: number } підходить для будь-якого типу, що вимагає { x: number }, навіть без явного оголошення.
Q: Як TypeScript працює з JS-бібліотеками без типів?
A: Через пакети @types/, наприклад @types/react або @types/node. Це .d.ts-файли, які описують форму бібліотеки без TypeScript-коду в самій бібліотеці.
Q: Чи варто завжди вмикати strict-режим?
A: Так, в нових проєктах. strict: true у tsconfig.json вмикає strictNullChecks, noImplicitAny та ще кілька перевірок, які ловлять найпоширеніші баги. Без них TypeScript - тільки назва.
Q: Як мігрувати JS-проєкт на TypeScript без поломки CI?
A: Встав allowJs: true і checkJs: false у tsconfig.json. Додавай // @ts-check в окремі файли поступово. Так .js і .ts файли спокійно співіснують під час переходу.
Приклади
Типізований React-компонент
interface UserProps {
id: number;
name: string;
isAdmin: boolean;
}
const UserCard: React.FC<UserProps> = ({ id, name, isAdmin }) => (
<div>
{name} (ID: {id}){isAdmin && <strong> Admin</strong>}
</div>
);
// TypeScript ловить це до завантаження в браузері:
// <UserCard id="1" name="Alice" isAdmin={false} />
// Помилка: Type 'string' is not assignable to type 'number'Передати id="1" (рядок) замість id={1} (число) - один з найпоширеніших багів у React. TypeScript блокує це на етапі компіляції. Такий патерн зустрічається в кожному Next.js або Remix проєкті з типізованими компонентами.
Discriminated union в API-обробнику
type ApiResponse =
| { success: true; data: string }
| { success: false; error: string };
function handleUser(id: number): ApiResponse {
if (id > 0) {
return { success: true, data: 'User found' };
}
return { success: false, error: 'Invalid ID' };
}
const result = handleUser(1);
if (result.success) {
console.log(result.data); // TypeScript знає: тут є 'data'
} else {
console.log(result.error); // А тут є 'error'
}Поле success виступає дискримінатором (discriminator). TypeScript звужує тип всередині кожної гілки, тому доступ до .data у гілці помилки - це помилка компіляції, а не runtime-сюрприз. Цей підхід часто зустрічається в NestJS і Express обробниках.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.