Skip to main content

Чому потрібен TypeScript, переваги та недоліки

TypeScript - це надбудова над JavaScript, яка додає статичні типи і виявляє помилки ще до запуску коду.

Теорія

TL;DR

  • JavaScript падає в runtime, коли передаєш дані не того типу; TypeScript показує помилку в редакторі ще під час написання
  • Аналогія: JavaScript - це виписати чек без перевірки балансу; TypeScript - банківський застосунок, який блокує овердрафт до підтвердження
  • TypeScript компілюється в звичайний JavaScript, тому браузери і Node.js запускають його без змін
  • Використовуй TS для команд від 5+ людей або проєктів від 10k рядків; для швидких скриптів JS достатньо

Швидкий приклад

typescript
// 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
// Неправильно: зводить нанівець весь сенс 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 вимкнений

typescript
// Неправильно (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

typescript
// 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-компонент

typescript
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-обробнику

typescript
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 обробниках.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?