1. Поясніть, що таке TypeScript та які його ключові відмінності від JavaScript.
TypeScript — це надбудова над JavaScript, яка додає статичну типізацію, інтерфейси та інші можливості для підвищення надійності коду.
-
Типізація: TS має статичні типи, JS — динамічні.
-
Розробка: TS виявляє помилки на етапі компіляції, JS — під час виконання.
-
Сумісність: TS компілюється у JS, тому працює у всіх середовищах JS.
-
Інструменти: краща підтримка IDE (автодоповнення, рефакторинг).
2. Що означає твердження, що TypeScript є надмножиною JavaScript?
- Це означає, що будь-який коректний JavaScript-код є також коректним TypeScript-кодом. TypeScript розширює можливості JS, додаючи типи та інші фічі, але при цьому не змінює базову мову.
3. Які основні вбудовані типи даних підтримує TypeScript?
Основні типи в TypeScript:
-
string
— рядки -
number
— числа (цілі та з плаваючою крапкою) -
boolean
— логічні значення -
null
таundefined
-
any
— будь-який тип -
unknown
— невідомий тип (безпечніша альтернатива any) -
void
— відсутність значення (часто у функціях) -
never
— функція ніколи не повертає значення (наприклад, кидає помилку) -
object
— об’єкти -
Масиви
(type[] або Array) -
Кортежі
([type1, type2, ...]) -
enum
— перерахування
4. Які способи оголошення змінних у TypeScript та як до них застосовується типізація?
У TypeScript змінні оголошуються так само, як у JavaScript: let
, const
,
рідше var
.
Тип можна:
- вивести автоматично (Type Inference):
let age = 25; // type: number
- задати явно:
let age: number = 25;
const name: string = "Alice";
Зазвичай рекомендують використовувати const
для незмінних значень, let
для
змінних, а явну типізацію — там, де виведення типу неочевидне.
5. Що таке інтерфейси в TypeScript і для чого вони використовуються?
Інтерфейси в TypeScript описують структуру об’єкта (його властивості та їх типи), не створюючи конкретної реалізації. Вони допомагають забезпечити контракт між частинами коду.
Основні можливості:
- Опис форми об’єкта:
interface User {
id: number;
name: string;
isAdmin?: boolean; // необов’язкове поле
}
const user: User = { id: 1, name: "Alice" };
-
Підтримка опціональних властивостей (?).
-
Можливість розширення (extends).
-
Використання для опису структур функцій, класів та масивів.
По суті, інтерфейси — це спосіб зробити код більш передбачуваним і безпечним.
6. Що таке enum у TypeScript та в яких випадках його доцільно використовувати?
enum
(перерахування) — це тип, який дозволяє задати набір іменованих констант.
- Numeric enum (значення автоматично інкрементуються):
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
- String enum:
enum Role {
Admin = "ADMIN",
User = "USER",
Guest = "GUEST"
}
Використовується, коли є обмежений набір варіантів (напр. ролі користувачів, статуси замовлення, напрямки руху). Це робить код більш читабельним і безпечним, ніж "магічні числа" чи рядки.
7. Як правильно оголошувати та використовувати функції в TypeScript із врахуванням типів?
- Функції визначаються так само, як у JavaScript, але в TypeScript можна явно задавати типи параметрів і результату:
// З явними типами
function add(a: number, b: number): number {
return a + b;
}
// Функціональний вираз
const greet = (name: string): string => {
return `Hello, ${name}`;
};
// Необов’язковий параметр
function log(message: string, userId?: number): void {
console.log(message, userId);
}
-
Параметри можна робити обов’язковими, необов’язковими (?) або мати значення за замовчуванням.
-
Тип повернення можна вивести автоматично, але для складних функцій краще вказувати явно.
-
Для callback-ів та складних сигнатур використовують типи або інтерфейси функцій.
8. Що таке виведення типу (type inference) у TypeScript і як воно працює?
- Виведення типу — це механізм, коли TypeScript автоматично визначає тип змінної чи результату функції на основі наданого значення без явного оголошення.
let count = 10; // TS виводить: number
let message = "Hi"; // TS виводить: string
function add(a: number, b: number) {
return a + b; // TS виводить: number (тип повернення)
}
-
Перевага: менше коду, але збережена типобезпека.
-
Ризик: у складних випадках краще явно вказувати тип, щоб уникнути неочікуваного any.
9. У чому різниця між let і const у TypeScript і як правильно їх використовувати?
let
— дозволяє оголосити змінну, значення якої можна змінювати. Має блочну
область видимості.
const
— створює змінну, якій можна призначити значення лише один раз. Також
має блочну область видимості.
let counter: number = 1;
counter = 2; // ✅ можна
const name: string = "Alice";
name = "Bob"; // ❌ помилка
Рекомендація: за замовчуванням використовувати const
, а let
— лише коли
змінна дійсно змінюється.
Важливо: const
не робить об’єкт immutable, змінювати внутрішні властивості все
одно можна:
const user = { id: 1, name: "Alice" };
user.name = "Bob"; // ✅ дозволено
10. Яким чином можна скомпілювати TypeScript-файли у JavaScript?
-
Використовується TypeScript Compiler (tsc).
-
Основні варіанти:
# компіляція одного файлу
tsc file.ts
# компіляція проєкту з налаштуваннями tsconfig.json
tsc
-
У tsconfig.json можна задати цільову версію JS (target), директорію виводу (outDir), модулі (module) тощо.
-
Також можна включити watch mode:
tsc -w
У реальних проєктах часто використовують Babel, Webpack, Vite чи ts-node для інтеграції компіляції у збірку чи запуск коду напряму.
11. Що таке класи в TypeScript і чим вони відрізняються від класів у звичайному JavaScript?
Класи в TypeScript — це надбудова над JS-класами. Вони працюють так само, як у JS, але доповнені системою типів:
-
можна оголошувати типи для полів, параметрів і повертаних значень;
-
є модифікатори доступу (public, private, protected, readonly);
-
є abstract класи та методи;
-
підтримка implements для інтерфейсів;
-
підтримка generics.
У рантаймі вони компілюються в звичайні JS-класи, а типи прибираються.
12. Як правильно реалізувати спадкування класів у TypeScript?
Використовується ключове слово extends
. Базовий клас може мати загальні
властивості/методи, похідний — успадковує їх і може перевизначати. При
перевизначенні конструктора обов’язково викликається super()
.
class Animal {
constructor(public name: string) {}
speak(): void {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name);
}
speak(): void {
console.log(`${this.name} barks.`);
}
}
const rex = new Dog("Rex", "Labrador");
rex.speak(); // Rex barks.
13. Які є модифікатори доступу в TypeScript і як вони впливають на властивості та методи класів?
TypeScript має 4 модифікатори доступу:
-
public
(за замовчуванням) – доступний скрізь. -
private
– доступний тільки всередині цього класу. -
protected
– доступний у класі та його нащадках. -
readonly
– властивість доступна тільки для читання після ініціалізації.
Вони впливають лише на етапі компіляції (для контролю типів), у рантаймі JavaScript цього обмеження немає.
14. Що таке абстрактні класи в TypeScript і для чого їх використовують?
Абстрактний клас — це клас, який не можна інстанціювати напряму. Він може містити:
-
реалізовані методи, які спільні для всіх нащадків,
-
abstract методи без реалізації, які зобов’язані реалізувати похідні класи.
Призначення: задавати загальний контракт і базову поведінку для групи класів, залишаючи конкретну реалізацію нащадкам.
abstract class Shape {
constructor(public color: string) {}
abstract area(): number; // має реалізувати підклас
describe(): void {
console.log(`This shape is ${this.color}`);
}
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
const c = new Circle("red", 5);
c.describe(); // This shape is red
console.log(c.area()); // 78.5398...
15. Як працюють конструктори в класах TypeScript і які особливості їх використання?
Конструктор (constructor) — це метод для ініціалізації об’єкта класу. Особливості у TypeScript:
-
можна задавати типи параметрів;
-
можна використовувати модифікатори доступу прямо в параметрах (
public
,private
,protected
,readonly
) — тоді TypeScript автоматично створює відповідні поля; -
у похідних класах обов’язково викликається
super()
перед використаннямthis
.
class Person {
constructor(public name: string, private age: number) {}
greet() {
console.log(`Hi, my name is ${this.name}`);
}
}
class Employee extends Person {
constructor(name: string, age: number, public position: string) {
super(name, age);
}
}
const emp = new Employee("Alice", 30, "Developer");
emp.greet(); // Hi, my name is Alice
console.log(emp.position); // Developer
16. Що таке декоратори в TypeScript для властивостей класу і як їх застосовувати?
Декоратори — це функції, які дозволяють змінювати або розширювати поведінку класів, методів, властивостей або параметрів. Декоратор властивості отримує ціль (target) та ім’я властивості (property key).
Приклад використання властивості:
function logProperty(target: any, key: string) {
let value = target[key];
const getter = () => {
console.log(`Getting ${key}: ${value}`);
return value;
};
const setter = (newVal: any) => {
console.log(`Setting ${key} to ${newVal}`);
value = newVal;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class Person {
@logProperty
name: string = "";
}
const p = new Person();
p.name = "Alice"; // Setting name to Alice
console.log(p.name); // Getting name: Alice
Декоратори часто використовують для логування, валідації, DI (dependency injection) та метаданих.
17. Як реалізуються та працюють геттери (get) і сеттери (set) у TypeScript?
Геттери та сеттери дозволяють контролювати доступ до властивостей класу.
-
get
— повертає значення властивості, дозволяє виконувати додаткову логіку при читанні. -
set
— задає значення властивості, дозволяє перевіряти або модифікувати його перед присвоєнням.
class Person {
private _age: number = 0;
get age(): number {
return this._age;
}
set age(value: number) {
if (value < 0) throw new Error("Age cannot be negative");
this._age = value;
}
}
const p = new Person();
p.age = 25; // викликається set
console.log(p.age); // викликається get -> 25
Геттери і сеттери працюють як звичайні властивості при доступі, але дозволяють інкапсулювати логіку.
18. Як реалізується перевантаження методів у TypeScript?
TypeScript дозволяє перевантажувати методи через сигнатури, але тільки одна реалізація. Це означає: можна оголосити кілька варіантів виклику методу з різними параметрами, а в тілі методу реалізувати логіку з перевіркою типів/кількості аргументів.
class Calculator {
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: any, b: any): any { // реальна реалізація
return a + b;
}
}
const calc = new Calculator();
console.log(calc.add(2, 3)); // 5
console.log(calc.add("Hello, ", "TS")); // Hello, TS
-
Сигнатури визначають дозволені варіанти виклику.
-
Реалізація повинна враховувати всі варіанти.
-
У рантаймі перевантаження як у C#/Java не існує, це чисто типізаційний механізм.
19. Для чого використовується ключове слово static у класах TypeScript?
static
дозволяє створювати члени класу (властивості або методи), які належать
самому класу, а не його екземплярам.
-
До них звертаються через ім’я класу (ClassName.member), а не через об’єкт.
-
Можна використовувати для констант, утилітарних методів та лічильників.
class Counter {
static count = 0;
static increment() {
Counter.count++;
}
}
Counter.increment();
console.log(Counter.count); // 1
const c = new Counter();
// c.increment(); // ❌ помилка, increment — static
20. Як створити власний тип у TypeScript за допомогою псевдоніму (type alias)?
Псевдонім типу (type
) дозволяє створити нове ім’я для будь-якого типу, включно
з об’єднаннями (union
), перетинами (intersection
) та функціями. Це зручно
для складних типів, повторного використання і документації коду.
type ID = string | number;
type User = {
id: ID;
name: string;
age?: number; // необов’язкове поле
};
type Callback = (result: string) => void;
Використовуємо як звичайний тип:
const user: User = { id: 1, name: "Alice" };
Псевдоніми не створюють нових типів у рантаймі — це чисто типізація на етапі компіляції.
21. Що таке union-типи в TypeScript і як їх застосовувати?
Union-тип (|) дозволяє змінній або параметру приймати декілька можливих типів. Це зручно, коли значення може бути різного виду.
type ID = string | number;
function printId(id: ID) {
if (typeof id === "string") {
console.log("ID (string): " + id.toUpperCase());
} else {
console.log("ID (number): " + (id * 10));
}
}
printId("abc"); // ID (string): ABC
printId(123); // ID (number): 1230
-
Потрібно робити type narrowing (перевірку типу) перед використанням специфічних методів.
-
Можна комбінувати кілька типів, навіть
null | undefined
.
22. Що таке intersection-типи в TypeScript і як вони працюють?
Intersection-тип (&) поєднує кілька типів в один. Об’єкт повинен відповідати всім об’єднаним типам одночасно. Це зручно для створення складних структур з кількох контрактів.
type Person = { name: string };
type Employee = { company: string };
type Developer = Person & Employee & { skills: string[] };
const dev: Developer = {
name: "Alice",
company: "TechCorp",
skills: ["TypeScript", "React"]
};
-
Якщо є конфліктні властивості з різними типами → результат може стати never.
-
Добре поєднується з interface і type для композиції.
23. Що таке кортежі (Tuple types) у TypeScript і в яких випадках їх варто застосовувати?
Tuple — це масив із фіксованою кількістю елементів та визначеними типами для кожної позиції. Використовуються, коли порядок і типи елементів наперед відомі.
let user: [number, string, boolean];
user = [1, "Alice", true]; // ✅ правильний порядок і типи
user = ["Alice", 1, true]; // ❌ помилка
- Можна додати назви для кращої читабельності:
type HttpResponse = [statusCode: number, message: string];
const res: HttpResponse = [200, "OK"];
- Підтримують optional та rest елементи:
type RGB = [number, number, number?, number?]; // (R, G, B, A?)
Використовувати, коли треба передавати структуровані дані з фіксованим форматом (наприклад, координати, записи логів, HTTP-відповідь).
24. Що таке твердження типів (type assertions) у TypeScript і для чого вони потрібні?
Type assertion — це спосіб сказати компілятору: «повір мені, я знаю реальний тип цього значення». Це не змінює рантайм-поведінку, лише впливає на перевірку типів.
let value: unknown = "Hello TS";
// спосіб 1
let strLength: number = (value as string).length;
// спосіб 2 (JSX несумісний, тому рідше)
let strLength2: number = (<string>value).length;
-
коли TypeScript не може вивести точний тип;
-
при роботі з any або unknown;
-
при доступі до DOM-елементів:
const input = document.getElementById("username") as HTMLInputElement;
console.log(input.value);
25. Як працює перевірка типів за допомогою typeof у TypeScript і як її використовувати для type narrowing
typeof
у TypeScript використовується для звуження union-типів під час
виконання. Це type guard, який дозволяє компілятору зрозуміти, який тип у
змінної в конкретній гілці коду.
function printId(id: string | number) {
if (typeof id === "string") {
console.log("Uppercase ID:", id.toUpperCase()); // тут id: string
} else {
console.log("Numeric ID:", id.toFixed(2)); // тут id: number
}
}
-
typeof
перевіряє типи рантайму:string
,number
,boolean
,object
,function
,undefined
,symbol
,bigint
. -
Використовується у функціях для безпечної роботи з union-типами.
Також typeof можна використовувати для отримання типу змінної чи функції при оголошенні:
let person = { name: "Alice", age: 30 };
type Person = typeof person; // { name: string; age: number }
26. Чи можна в TypeScript створювати типи на основі існуючих даних (значень) за допомогою виведення типів?
Так, можна. TypeScript дозволяє виводити типи з існуючих значень за допомогою typeof і keyof.
- Отримання типу з об’єкта
const user = {
id: 1,
name: "Alice",
isAdmin: true
};
type User = typeof user;
// User = { id: number; name: string; isAdmin: boolean }
- Отримання типів ключів
type UserKeys = keyof typeof user;
// "id" | "name" | "isAdmin"
- Комбінація з літеральними типами
const roles = ["admin", "user", "guest"] as const;
type Role = typeof roles[number];
// "admin" | "user" | "guest"
Це дозволяє уникати дублювання коду й гарантує синхронізацію типів з даними.
27. Що таке узагальнені типи (Generics) у TypeScript і для чого вони потрібні?
Generics — це параметризовані типи, які дозволяють писати універсальний і багаторазовий код, зберігаючи типобезпеку. Вони дозволяють відкладати визначення конкретного типу до моменту використання.
function identity(value: any): any {
return value;
}
- Проблема: втрачається тип.
function identity<T>(value: T): T {
return value;
}
let num = identity<number>(42); // num: number
let str = identity("Hello"); // str: string (TS вивів тип автоматично)
class Box<T> { constructor(public content: T) {}
}
const stringBox = new Box("TS"); // Box<string>
const numberBox = new Box(123); // Box<number>
-
Писати гнучкий і типобезпечний код (колекції, утиліти, API).
-
Уникати any і втрати інформації про тип.
-
Дозволяє зв’язати вхідний і вихідний типи.
28. Як правильно створити узагальнену (generic) функцію в TypeScript?
Узагальнена функція визначається через параметр типу в кутових дужках <T>
. Це
дозволяє зберегти типобезпеку і не втрачати інформацію про тип.
function identity<T>(value: T): T {
return value;
}
let n = identity<number>(10); // n: number
let s = identity("TS"); // s: string (тип виведено автоматично)
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("id", 123); // [string, number]
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("Hello"); // 5
getLength([1, 2, 3]); // 3
getLength(42); // ❌ помилка, бо number не має length
Таким чином, generics роблять функції універсальними, але строго типізованими.
29. Як визначити узагальнені (generic) інтерфейси у TypeScript і для чого вони використовуються?
Узагальнені інтерфейси дозволяють описати контракт, який працює з різними
типами, зберігаючи типобезпеку. Для цього в інтерфейс додають параметри типів
<T>
(або кілька).
interface Box<T> {
value: T;
}
const numBox: Box<number> = { value: 42 };
const strBox: Box<string> = { value: "Hello" };
interface Pair<K, V> {
key: K;
value: V;
}
const pair: Pair<string, number> = { key: "age", value: 30 };
interface Repository<T> {
getAll(): T[];
getById(id: number): T | null;
}
class UserRepo implements Repository<{ id: number; name: string }> {
private users = [{ id: 1, name: "Alice" }];
getAll() { return this.users; }
getById(id: number) { return this.users.find(u => u.id === id) ?? null; }
}
-
дозволяють будувати універсальні API (репозиторії, сервіси, колекції);
-
зберігають зв’язок між типами в методах/властивостях;
-
уникання дублювання коду для різних сутностей.
30. Як працюють узагальнені (generic) типи у класах TypeScript і як їх застосовувати?
У TypeScript можна робити класи параметризованими типами, додаючи параметр <T>
після імені класу. Це дозволяє створювати універсальні класи, які працюють з
різними типами даних, зберігаючи типобезпеку.
class Box<T> {
constructor(public content: T) {}
getContent(): T {
return this.content;
}
}
const numberBox = new Box<number>(123);
const stringBox = new Box<string>("Hello");
console.log(numberBox.getContent()); // 123
console.log(stringBox.getContent()); // Hello
class Pair<K, V> {
constructor(public key: K, public value: V) {}
}
const pair = new Pair<string, number>("id", 42);
class Collection<T extends { id: number }> {
private items: T[] = [];
add(item: T) { this.items.push(item); }
getById(id: number): T | undefined {
return this.items.find(i => i.id === id);
}
}
const users = new Collection<{ id: number; name: string }>();
users.add({ id: 1, name: "Alice" });
-
Універсальність класів без втрати типобезпеки.
-
Повторне використання логіки для різних типів.
-
Зв’язок між методами і властивостями через один параметр типу.
31. Як реалізувати узагальнене обмеження (generic constraint) у TypeScript і для чого воно потрібне?
У TypeScript можна обмежити generic-параметр за допомогою extends, щоб він повинен був відповідати певному типу або інтерфейсу. Це дозволяє безпечно використовувати властивості або методи об’єкта всередині функції або класу.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // ✅ рядок має length
logLength([1, 2, 3]); // ✅ масив має length
logLength(42); // ❌ помилка, number не має length
class Collection<T extends { id: number }> {
private items: T[] = [];
add(item: T) { this.items.push(item); }
getById(id: number): T | undefined {
return this.items.find(i => i.id === id);
}
}
const users = new Collection<{ id: number; name: string }>();
users.add({ id: 1, name: "Alice" }); // ✅ ok
-
Дозволяє використовувати властивості або методи об’єкта без перевірок типу.
-
Зберігає універсальність функцій і класів, але обмежує використання тільки сумісними типами.
32. Що таке дискримінований союз (Discriminated Union) у TypeScript і як він працює?
Discriminated Union — це патерн, коли union
типів має спільну
властивість-дискримінатор (зазвичай літеральний тип), яка дозволяє компілятору
звузити тип під час перевірок.
type Circle = {
kind: "circle";
radius: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
-
kind
(або інша властивість) має літеральне значення, унікальне для кожного варіанту. -
Це дозволяє TypeScript робити type narrowing автоматично у
switch
чиif
. -
Використовується для моделювання станів, подій, результатів API.
Фактично, це спосіб реалізації type-safe "enum-like" варіантів з різними структурами даних.
33. Що таке утилітний тип Readonly у TypeScript і як його оголосити/використати?
Readonly<T>
— це вбудований утилітний тип, який робить усі властивості об’єкта
тільки для читання (неможливо змінювати після ініціалізації).
type User = {
id: number;
name: string;
};
const u: Readonly<User> = {
id: 1,
name: "Alice"
};
u.name = "Bob"; // ❌ Помилка: властивість доступна тільки для читання
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
- Тобто це mapped type, який додає модифікатор readonly до кожної властивості.
Використовується для іммутабельних даних, DTO та запобігання випадковим змінам.
34. Що таке mapped types у TypeScript і як їх використовувати?
Mapped types — це спосіб створювати нові типи на основі існуючих, проходячи
по ключах (keyof
) та трансформуючи їх. Це використовується для створення
утилітних типів (Readonly
, Partial
, Pick
тощо).
type User = {
id: number;
name: string;
active: boolean;
};
// Робимо всі властивості readonly
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
-
readonly
/-readonly
→ додає або прибирає "тільки для читання" -
?
/-?
→ робить поле опціональним або обов’язковим
type PartialUser = {
[K in keyof User]?: User[K];
};
type MyMapped<T> = {
[P in keyof T]: T[P];
};
type Test = MyMapped<{ a: string; b: number }>;
// { a: string; b: number }
-
Readonly<T>
→ робить усі властивості readonly -
Partial<T>
→ робить усі властивості опціональними -
Required<T>
→ робить усі властивості обов’язковими -
Record<K, T>
→ створює об’єкт, де всі ключі мають значення типу T
Mapped types корисні для масових перетворень типів без дублювання коду.
35. Що таке умовні типи (Conditional Types) у TypeScript і як вони працюють?
Умовні типи дозволяють описувати залежності між типами за допомогою конструкції T extends U ? X : Y.
-
Якщо T підтип U, результат буде X.
-
Інакше — Y.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number>; // number
type ApiResponse<T> = T extends Error ? { success: false; error: T }
: { success: true; data: T };
type R1 = ApiResponse<string>; // { success: true; data: string }
type R2 = ApiResponse<Error>; // { success: false; error: Error }
-
Працюють у поєднанні з generics, union та mapped types.
-
Часто використовуються у вбудованих утилітах:
-
Exclude<T, U>
-
Extract<T, U>
-
NonNullable<T>
-
Умовні типи — це основа для гнучкої метапрограмінгової типізації.
36. Що таке індексні типи (Indexed Access Types) у TypeScript і як працює ключове слово keyof?
keyof
-
keyof створює об’єднання (union) ключів заданого типу.
-
Використовується для обмеження значень ключами інтерфейсу/типу.
type User = { id: number; name: string; active: boolean };
type UserKeys = keyof User;
// "id" | "name" | "active"
- Дозволяють отримати тип значення за конкретним ключем.
type UserIdType = User["id"]; // number
type UserNameOrActive = User["name" | "active"]; // string | boolean
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: "Alice", active: true };
let nameValue = getValue(user, "name"); // string
let activeValue = getValue(user, "active"); // boolean
-
Для generic-утиліт, які працюють із довільними об’єктами.
-
Для побудови type-safe доступу до властивостей.
-
Основа для утилітних типів (Pick, Omit, Record тощо).
37. У чому різниця між приведенням типів (type casting) і твердженням типів (type assertion) у TypeScript?
- Твердження типів (Type Assertion)
-
Це інструкція для компілятора, що значення має певний тип.
-
Не змінює значення у рантаймі.
-
Використовується, коли розробник краще знає тип, ніж TypeScript.
let value: unknown = "Hello";
let strLength = (value as string).length; // "повір, це string"
- Приведення типів (Type Casting, runtime cast)
-
Це перетворення значення в інший тип у рантаймі (наприклад, Number("123") → 123).
-
Виконується реальною функцією чи оператором у JS.
let str = "123";
let num = Number(str); // runtime casting → 123
-
Type assertion: впливає тільки на компіляцію, ніяких змін у рантаймі.
-
Type casting: реально змінює значення під час виконання.
У TypeScript під "casting" часто мають на увазі type assertions, але це не одне й те саме.
38. Що таке утилітні типи Partial, Required, Readonly та Pick у TypeScript і для чого вони потрібні?
Partial<T>
- Робить усі властивості опціональними.
type User = { id: number; name: string; };
type PartialUser = Partial<User>;
// { id?: number; name?: string }
- Використовується для об’єктів оновлення/патчів.
Required<T>
- Робить усі властивості обов’язковими (знімає ?).
type UserOptional = { id?: number; name?: string; };
type RequiredUser = Required<UserOptional>;
// { id: number; name: string }
- Корисно для валидації, коли потрібен повний об’єкт.
Readonly<T>
- Робить усі властивості доступними тільки для читання.
type User = { id: number; name: string; };
type ReadonlyUser = Readonly<User>;
const u: ReadonlyUser = { id: 1, name: "Alice" };
u.name = "Bob"; // ❌ Помилка
- Застосовується для іммутабельних даних.
Pick<T, K>
- Вибирає підмножину властивостей з типу T.
type User = { id: number; name: string; active: boolean };
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
- Корисно для DTO, селекторів, відображення лише потрібних полів.
Усі вони побудовані на mapped types + keyof.
Найчастіше застосовуються для гнучкої типізації API, DTO, form state, патчів даних.
39. Що таке тип never у TypeScript і в яких випадках він застосовується?
never
-
Це спеціальний тип, який означає значення, що ніколи не існує.
-
Використовується там, де функція або вираз не повертає значення взагалі.
- Функція, яка ніколи не завершується успішно
function fail(message: string): never {
throw new Error(message);
}
- Функція з нескінченним циклом
function infiniteLoop(): never {
while (true) {}
}
- Exhaustive checking (перевірка вичерпності union-типів)
type Shape = { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
default:
const _exhaustiveCheck: never = shape; // якщо додати новий варіант → помилка
return _exhaustiveCheck;
}
}
-
never
— підтип будь-якого типу, але жоден тип не є підтипом never (крім нього самого). -
Використовується для строгих перевірок типів і ситуацій, де значення бути не може.
never корисний у type-safe error handling та для гарантій повного покриття union-типів.
40. Як організувати код за допомогою модулів у TypeScript?
Основи модулів у TypeScript
-
Кожен файл з import або export стає модулем.
-
Використовуються ключові слова export та import (як у ES6).
math.ts
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14;
app.ts
import { add, PI } from "./math";
console.log(add(2, 3)); // 5
console.log(PI); // 3.14
// logger.ts
export default function log(msg: string) {
console.log("LOG:", msg);
}
// app.ts
import log from "./logger";
log("hello");
import * as MathUtils from "./math";
console.log(MathUtils.add(1, 2));
-
Файлова структура: групувати код за доменами (наприклад, services/, models/, utils/).
-
Barrel files (індексні модулі): об’єднувати кілька експортувань в одному файлі.
// utils/index.ts
export * from "./math";
export * from "./logger";
// app.ts
import { add, PI } from "./utils";
-
У
tsconfig.json
можна налаштувати:-
"module": (esnext, commonjs, amd, залежно від оточення).
-
"baseUrl", "paths": для зручних alias-імпортів.
-
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@utils/*": ["utils/*"]
}
}
}
Модулі в TypeScript = ті самі ES6 модулі, але з повною підтримкою типів.
41. У яких випадках доцільно використовувати простори імен (namespace) у TypeScript
-
Це спосіб групувати логіку всередині одного глобального об’єкта.
-
Використовувалися до появи модулів для уникнення колізій у глобальному просторі імен.
namespace Utils {
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14;
}
console.log(Utils.add(2, 3));
-
У legacy-проєктах або коли немає системи модулів (наприклад, код вбудовується напряму в
<script>
безbundler
). -
Для простих демо/маленьких проєктів, де немає потреби в модульній структурі.
-
Для декларацій
.d.ts
файлів, щоб групувати типи/інтерфейси.
-
У сучасному TypeScript стандартом є ES6 модулі (import/export).
-
Bundlers (Webpack, Vite, esbuild) та Node.js працюють із модулями, а не namespace.
-
Простори імен у великих проєктах ускладнюють масштабування.
-
Нові проєкти → використовувати модулі.
-
Простори імен → тільки у специфічних випадках (legacy, declaration merging, глобальні бібліотеки без модулів).
42. У чому різниця між внутрішніми та зовнішніми модулями в TypeScript?
- Внутрішні модулі (старий підхід)
-
Використовували ключове слово namespace.
-
Код групується в один глобальний об’єкт.
-
Завантаження відбувається через
<script>
без системи модулів.
namespace Utils {
export function add(a: number, b: number) {
return a + b;
}
}
console.log(Utils.add(2, 3));
Зараз вважаються застарілими — замінені на ES6-модулі.
- Зовнішні модулі (сучасний підхід)
-
Використовують export / import (ES6).
-
Кожен файл із export → модуль.
-
Працюють із bundlers, Node.js, Deno.
// math.ts
export function add(a: number, b: number) {
return a + b;
}
// app.ts
import { add } from "./math";
console.log(add(2, 3));
-
Внутрішні (namespace) → організація всередині одного глобального простору.
-
Зовнішні (modules) → організація через систему файлів з ізольованим простором імен.
-
Використовуємо зовнішні модулі (ES6/TypeScript import/export).
-
Внутрішні модулі (namespace) лишилися тільки для legacy та .d.ts декларацій.
43. Як у TypeScript експортувати та імпортувати модулі?
- Іменований експорт
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14;
// app.ts
import { add, PI } from "./math";
console.log(add(2, 3), PI);
- Можна імпортувати тільки потрібне.
- Експорт за замовчуванням (default)
// logger.ts
export default function log(message: string) {
console.log("LOG:", message);
}
// app.ts
import log from "./logger";
log("hello");
- Імпортується без {}, ім’я можна змінювати довільно.
- Перейменування при імпорті/експорті
// math.ts
export { add as sum };
// app.ts
import { sum as addNumbers } from "./math";
- Імпорт у вигляді простору імен
// app.ts
import * as MathUtils from "./math";
console.log(MathUtils.add(2, 3));
- Повторний експорт (re-export)
// utils.ts
export * from "./math";
export { default as log } from "./logger";
// app.ts
import { add, PI, log } from "./utils";
-
Використовувати іменований експорт для кількох сутностей.
-
Використовувати default для однієї "головної" сутності з файлу.
44. Що таке роздільна здатність модулів (module resolution) у TypeScript і які існують стратегії?
Це алгоритм, за яким TypeScript знаходить файл, що відповідає шляху з import або require. Приклад:
import { add } from "./math";
TypeScript має зрозуміти, чи це math.ts, math.d.ts, math.js чи інший файл.
- Classic (старий режим, до ES6)
-
Працює подібно до компілятора C/C++.
-
Використовується для старих скриптів, без node_modules.
-
Пошук іде відносно файлу, де зроблений імпорт.
Використовується рідко, в legacy-коді.
- Node (за замовчуванням)
-
Імітує механізм Node.js.
-
Шукає файл у такому порядку:
-
./module.ts
-
./module.tsx
-
./module.d.ts
-
./module/package.json
(types
абоmain
) -
./module/index.ts
-
./module/index.d.ts
-
Використовується у більшості сучасних проєктів.
У tsconfig.json:
{
"compilerOptions": {
"moduleResolution": "node" // або "classic"
}
}
"baseUrl"
– вказує базову директорію для відносних шляхів.
"paths"
– дозволяє створювати alias-и для імпортів.
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@utils/*": ["utils/*"]
}
}
}
import { add } from "@utils/math";
Classic
– для старих проєктів без модульної системи.
Node
– стандарт для сучасних TypeScript/Node.js застосунків.
45. Як модулі TypeScript сумісні з модулями ES6?
TypeScript повністю базується на ES6-модулях:
-
import / export працюють так само, як у JS.
-
Кожен файл з import або export вважається модулем.
-
Під час компіляції TS може перетворювати код у різні системи модулів (CommonJS, ES6, AMD, UMD тощо).
TypeScript
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
Використання в ES6
import { add } from "./math.js";
console.log(add(2, 3));
Після компіляції з "module": "ESNext" у tsconfig.json результат буде ідентичним ES6.
TypeScript дозволяє імпортувати CommonJS-модулі:
import fs from "fs"; // default-імпорт
import * as path from "path"; // namespace-імпорт
Працює завдяки прапору "esModuleInterop": true у tsconfig.json.
- TypeScript → ES6
-
TS просто додає типи, які зникають при компіляції.
-
Залишається чистий ES6-код.
- Interop із CommonJS
- Можна імпортувати старі бібліотеки (require) без проблем.
- Default vs Named exports
-
ES6 → default експортується як export default.
-
TS дозволяє змішувати default та named (через esModuleInterop).
-
TypeScript сумісний із ES6-модулями 1:1.
-
Додатково підтримує CommonJS через компілятор.
-
Використання "module": "ESNext" і "esModuleInterop": true робить код максимально універсальним.
46. Що таке декоратори у TypeScript і як їх використовують?
Декоратори — це спеціальні функції, які можна застосовувати до класів, методів, властивостей або параметрів, щоб змінювати або розширювати їхню поведінку. Вони працюють як метадані + синтаксичний цукор над патерном higher-order functions.
- У TypeScript декоратори — експериментальна функція, вмикаються прапором:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
function MyDecorator(target: any) {
console.log("Декоратор застосовано до:", target);
}
@MyDecorator
class Example {}
- Класів
function LogClass(constructor: Function) {
console.log("Class:", constructor.name);
}
@LogClass
class User {}
- Методів
function LogMethod(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Call ${propertyKey} with`, args);
return original.apply(this, args);
};
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}
new Calculator().add(2, 3);
- Властивостей
function Readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, { writable: false });
}
class Car {
@Readonly
brand: string = "Tesla";
}
- Параметрів
function LogParam(target: any, method: string, index: number) {
console.log(`Param at index ${index} in method ${method}`);
}
class Service {
print(@LogParam msg: string) {
console.log(msg);
}
}
-
DI-фреймворки (NestJS, Angular) — для позначення сервісів, компонентів.
-
Логування, кешування, валідація.
-
Метадані (через reflect-metadata).
Декоратори — це функції-обгортки для класів та їх елементів, що дозволяють декларативно додавати поведінку.
47. Що таке декоратори класів у TypeScript і як вони змінюють поведінку класів?
Декоратор класу — це функція, яка отримує конструктор класу як аргумент. Він може:
-
додати метадані,
-
змінити або підмінити конструктор,
-
модифікувати/доповнити прототип.
type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
function LogClass(constructor: Function) {
console.log(`Клас створено: ${constructor.name}`);
}
@LogClass
class User {}
- При завантаженні модуля виведе: Клас створено: User.
function WithTimestamp(constructor: Function) {
constructor.prototype.timestamp = new Date();
}
@WithTimestamp
class Order {}
const o = new Order();
console.log(o.timestamp); // Дата створення
function Sealed<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
id = Math.random();
};
}
@Sealed
class Product {
name = "Book";
}
const p = new Product();
console.log(p.name, p.id); // "Book", 0.12345
- Декоратор створив новий клас, що розширює оригінальний.
-
Angular/NestJS:
@Component
,@Injectable
,@Module
. -
Логування, трейсинг: автоматично додавати поведінку.
-
Метадані: вказувати схеми валідації, ролі доступу тощо.
Декоратори класів дозволяють декларативно змінювати або розширювати клас (метадані, властивості, конструктор), що робить їх основою для DI та метапрограмування в TypeScript.
48. Що таке декоратори методів у TypeScript і як їх використовувати?
Декоратор методу — це функція, яка застосовується до методу класу. Він отримує:
-
target
— прототип класу (для екземплярного методу) або конструктор (для статичного). -
propertyKey
— ім’я методу. -
descriptor
— PropertyDescriptor, що описує метод (можна змінювати).
Використовується для перехоплення викликів, логування, кешування, валідації тощо.
type MethodDecorator = (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => void | PropertyDescriptor;
function LogMethod(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Виклик ${propertyKey} з аргументами:`, args);
return original.apply(this, args);
};
}
class Calculator {
@LogMethod
add(a: number, b: number) {
return a + b;
}
}
new Calculator().add(2, 3);
// Лог: "Виклик add з аргументами: [2, 3]"
function Once(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
let called = false;
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
if (called) {
console.log(`Метод ${propertyKey} вже викликано!`);
return;
}
called = true;
return original.apply(this, args);
};
}
class Service {
@Once
init() {
console.log("Ініціалізація...");
}
}
const s = new Service();
s.init(); // "Ініціалізація..."
s.init(); // "Метод init вже викликано!"
function CatchErrors(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await original.apply(this, args);
} catch (err) {
console.error(`Помилка у ${propertyKey}:`, err);
}
};
}
class Api {
@CatchErrors
async fetchData() {
throw new Error("Network error");
}
}
new Api().fetchData(); // Лог: "Помилка у fetchData: Error: Network error"
Декоратори методів у TypeScript дають можливість переписати або обгорнути метод (через PropertyDescriptor), що робить їх зручними для реалізації AOP-патернів (логування, кешування, обробка помилок, throttle/debounce).
49. Що таке декоратори аксесорів (get/set) у TypeScript і як вони працюють?
Декоратори аксесорів застосовуються до геттерів або сеттерів у класах. Вони працюють майже так само, як декоратори методів, але застосовуються до get/set.
- Сигнатура:
type AccessorDecorator = (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => void | PropertyDescriptor;
function LogAccessor(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGet = descriptor.get;
const originalSet = descriptor.set;
if (originalGet) {
descriptor.get = function () {
console.log(`Отримання значення ${propertyKey}`);
return originalGet.apply(this);
};
}
if (originalSet) {
descriptor.set = function (value: any) {
console.log(`Присвоєння ${propertyKey} = ${value}`);
return originalSet.apply(this, [value]);
};
}
}
class User {
private _name: string = "Anonymous";
@LogAccessor
get name() {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const u = new User();
console.log(u.name); // Лог: Отримання значення name
u.name = "Viktor"; // Лог: Присвоєння name = Viktor
function MinLength(length: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set!;
descriptor.set = function (value: string) {
if (value.length < length) {
throw new Error(`${propertyKey} має бути мінімум ${length} символів`);
}
originalSet.call(this, value);
};
};
}
class Product {
private _title: string = "";
@MinLength(3)
set title(value: string) {
this._title = value;
}
get title() {
return this._title;
}
}
const p = new Product();
p.title = "TV"; // ❌ Error: title має бути мінімум 3 символів
-
Декоратори аксесорів працюють з геттерами/сеттерами.
-
Дозволяють:
-
логувати доступ,
-
робити валідацію,
-
контролювати зміну значень.
-
-
Як і метод-декоратори, вони змінюють PropertyDescriptor.
50. Що таке декоратори властивостей у TypeScript і як їх використовувати?
Декоратор властивості застосовується до поля класу. На відміну від методів чи аксесорів, він не має доступу до PropertyDescriptor, оскільки властивості ще не існують на момент компіляції.
- Сигнатура:
type PropertyDecorator = (
target: Object,
propertyKey: string | symbol
) => void;
function LogProperty(target: any, propertyKey: string) {
console.log(`Властивість "${propertyKey}" додана у клас ${target.constructor.name}`);
}
class User {
@LogProperty
name: string;
constructor(name: string) {
this.name = name;
}
}
// Лог: Властивість "name" додана у клас User
function Required(target: any, propertyKey: string) {
if (!target.__required) {
target.__required = [];
}
target.__required.push(propertyKey);
}
class Product {
@Required
title: string;
@Required
price: number;
}
function validate(obj: any) {
const required = obj.__proto__.__required || [];
for (const key of required) {
if (obj[key] === undefined) {
throw new Error(`Поле ${key} є обов’язковим`);
}
}
}
const p = new Product();
p.title = "TV";
validate(p); // ❌ Error: Поле price є обов’язковим
function DefaultValue(value: any) {
return function (target: any, propertyKey: string) {
let val = value;
Object.defineProperty(target, propertyKey, {
get: () => val,
set: (newVal) => (val = newVal),
enumerable: true,
configurable: true,
});
};
}
class Settings {
@DefaultValue("light")
theme: string;
}
const s = new Settings();
console.log(s.theme); // "light"
s.theme = "dark";
console.log(s.theme); // "dark"
-
Декоратори властивостей працюють тільки з назвою властивості та прототипом класу.
-
Використовуються для:
-
логування,
-
додавання метаданих,
-
створення власних валідацій,
-
ініціалізації значень.
-
-
Для більш складних сценаріїв часто комбінуються з рефлексією (Reflect.metadata) або бібліотеками на кшталт class-validator.
51. Як декоратори допомагають компонувати (організовувати) код у TypeScript?
Декоратори — це спеціальні анотації для класів, методів, властивостей чи параметрів, які дозволяють додавати поведінку або метадані, не змінюючи безпосередньо бізнес-логіку. Вони реалізують принципи AOP (Aspect-Oriented Programming) — винесення повторюваних завдань (логування, валідація, DI) в окремі аспекти.
-
Виносять повторювану логіку (логування, кешування, валідацію) у незалежні функції.
-
Роблять код декларативним — замість "писати вручну" можна описати поведінку через анотацію.
-
Додають метадані до класів/методів/властивостей, які можна зчитувати у runtime.
-
Сприяють модульності — декоратор можна підключити/відключити без зміни основного коду.
Без декоратора:
class UserService {
getUser(id: number) {
console.log(`Викликано getUser з id=${id}`);
return { id, name: "Alice" };
}
}
З декоратором:
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Викликано ${propertyKey} з аргументами:`, args);
return original.apply(this, args);
};
}
class UserService {
@Log
getUser(id: number) {
return { id, name: "Alice" };
}
}
- Логіка логування винесена окремо, метод залишився чистим.
function Injectable(constructor: Function) {
Reflect.defineMetadata("injectable", true, constructor);
}
@Injectable
class UserService {}
@Injectable
class AuthService {
constructor(private userService: UserService) {}
}
- Декоратори дозволяють компонувати сервіси у єдину систему без ручного "склеювання".
-
Декоратори — це механізм композиції коду у TypeScript.
-
Вони дозволяють:
-
підключати поведінку без змін основного коду,
-
додавати метадані для фреймворків (DI, маршрутизація, ORM),
-
робити код чистішим і декларативним.
-
-
Використовуються в Angular, NestJS, TypeORM, MobX для організації архітектури.
52. Що таке рефлексія у TypeScript і як вона використовується разом із декораторами?
Рефлексія — це можливість коду отримувати метадані про себе під час
виконання (runtime). У TypeScript це реалізується через бібліотеку
reflect-metadata
, яка дозволяє зчитувати/записувати метадані для класів,
методів, властивостей і параметрів.
Декоратори не змінюють сам об’єкт напряму, а часто зберігають додаткову
інформацію у метаданих. Цю інформацію потім можна зчитати через
Reflect.getMetadata
.
import "reflect-metadata";
function MinLength(length: number) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata("minLength", length, target, propertyKey);
};
}
class User {
@MinLength(5)
username: string;
}
// Читання метаданих
const len = Reflect.getMetadata("minLength", User.prototype, "username");
console.log(len); // 5
- Декоратор
@MinLength
записує метадані, а валідаційна логіка може потім їх використовувати.
function Inject(token: string) {
return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
Reflect.defineMetadata("inject", token, target, `param_${parameterIndex}`);
};
}
class AuthService {
constructor(
@Inject("UserService") private userService: any
) {}
}
console.log(Reflect.getMetadata("inject", AuthService, "param_0"));
// "UserService"
- Через рефлексію фреймворк розуміє, який сервіс підставити у конструктор.
-
NestJS —
@Controller
,@Injectable
,@Param
,@Body
працюють на базіreflect-metadata
. -
TypeORM —
@Entity
,@Column
зберігають схему таблиці у метаданих. -
Angular —
@Injectable
та DI-контейнер також використовують метадані.
-
Рефлексія = механізм зберігання/зчитування метаданих у runtime.
-
Декоратори = зручний спосіб записувати метадані.
-
У поєднанні вони дозволяють будувати декларативні фреймворки (NestJS, Angular, TypeORM), де метадані визначають, як працює DI, маршрутизація чи ORM.
53. Що таке tsconfig.json і для чого він використовується у TypeScript?
tsconfig.json
— це конфігураційний файл TypeScript, який:
-
описує як компілювати проєкт (шлях до вихідних файлів, вихідна директорія, таргетовану версію JS тощо);
-
визначає налаштування компілятора (строгість типів, модульну систему, JSX);
-
дозволяє інструментам (IDE, build-системам) розуміти структуру проєкту.
{
"compilerOptions": {
"target": "ES2020", // версія JS на виході
"module": "ESNext", // система модулів
"strict": true, // включає сувору перевірку типів
"outDir": "./dist", // куди зберігати зкомпільовані файли
"rootDir": "./src", // де лежить вихідний код
"esModuleInterop": true, // коректний імпорт CommonJS-модулів
"skipLibCheck": true // пропускає перевірку *.d.ts
},
"include": ["src"], // які файли включати
"exclude": ["node_modules"] // які ігнорувати
}
-
Забезпечує єдині правила компіляції для всіх у проєкті.
-
Дозволяє типізувати код жорсткіше чи м’якше залежно від налаштувань.
-
Дає можливість інтегрувати TypeScript з Webpack, Babel, Jest, ts-node тощо.
tsconfig.json
= "контракт" між розробниками, компілятором і tooling’ом про те,
як саме треба збирати й перевіряти TypeScript-код.
54. Як у tsconfig.json вказати, які файли включати або виключати під час компіляції?
-
include
— список файлів/папок, які слід компілювати. -
exclude
— список файлів/папок, які потрібно ігнорувати. -
files
— явний перелік файлів (рідко використовується).
- Включення всіх файлів із src
{
"include": ["src"]
}
- Включення тільки .ts і .tsx файлів
{
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
- Виключення тестів і node_modules
{
"include": ["src"],
"exclude": ["node_modules", "**/*.test.ts"]
}
- Використання files для точного списку
{
"files": ["src/index.ts", "src/app.ts"]
}
- У такому разі компілюватимуться тільки ці файли, навіть якщо є інші.
-
Якщо є
files
→ беруться тільки вони. -
Якщо є
include
→ компілятор бере ці файли + всі залежності. -
exclude
завжди має вищий пріоритет і "вирізає" файли зinclude
.
55. Які найчастіше використовувані параметри в compilerOptions файлі tsconfig.json?
target
— версія JavaScript на виході
"target": "ES2020"
module
— система модулів
"module": "ESNext" // або CommonJS, UMD
strict
— вмикає всі суворі перевірки типів
"strict": true
outDir
— директорія для зкомпільованих файлів
"outDir": "./dist"
rootDir
— коренева папка вихідних файлів
"rootDir": "./src"
esModuleInterop
— коректний імпорт CommonJS-пакетів
"esModuleInterop": true
allowJs
— дозволяє компілювати .js файли разом із .ts
"allowJs": true
checkJs
— перевіряє типи у .js файлах
"checkJs": true
sourceMap
— створює .map для дебагу в браузері
"sourceMap": true
baseUrl
+paths
— налаштування alias-імпортів
"baseUrl": "./src",
"paths": {
"@components/*": ["components/*"]
}
skipLibCheck
— пропускає перевірку типів у *.d.ts
"skipLibCheck": true
resolveJsonModule
— дозволяє імпортувати .json файли
"resolveJsonModule": true
jsx
— режим для React/JSX
"jsx": "react-jsx" // або "react", "preserve"
Підсумок: найчастіше розробники у фронтенді змінюють target
, module
,
strict
, outDir
, jsx
, esModuleInterop
, baseUrl/paths
.
56. Як TypeScript підтримує source maps і для чого вони потрібні?
Source maps — це файли, які дозволяють браузеру або інструментам налагодження зіставляти зкомпільований JavaScript із оригінальним TypeScript-кодом.
-
Формат: .js.map
-
Дозволяє дебагати TS прямо в браузері або IDE, бачачи рядки та колонки TS, а не JS.
{
"compilerOptions": {
"sourceMap": true,
"outDir": "./dist"
}
}
- Після компіляції для кожного file.ts з’явиться file.js та file.js.map.
// src/app.ts
const msg: string = "Hello TS";
console.log(msg);
При компіляції з sourceMap: true:
-
app.js — згенерований JS
-
app.js.map — мапа для дебагу
-
В браузері можна ставити breakpoints у app.ts, а не у JS.
-
Chrome DevTools / Firefox Debugger — автоматично підхоплюють .map.
-
Webpack / Vite — інтегрують TS source maps у bundle для фронтенду.
-
Node.js (з ts-node або source-map-support) — дебаг TS на сервері.
inlineSourceMap: true
— вставляє карту прямо в JS файл.
inlineSources: true
— вставляє оригінальний TS-код у .map.
{
"compilerOptions": {
"inlineSourceMap": true,
"inlineSources": true
}
}
Source maps у TypeScript дозволяють дебагувати оригінальний код після компіляції, зберігаючи зв’язок між TS та JS.
57. Що таке поступова (incremental) збірка в TypeScript і як вона працює?
Поступова збірка (Incremental build) — це режим компілятора TypeScript, який дозволяє компілювати тільки ті файли, які змінилися, замість повної перекомпіляції всього проєкту.
-
Зменшує час збірки великих проєктів.
-
Зберігає інформацію про попередню збірку у файлі .tsbuildinfo.
{
"compilerOptions": {
"incremental": true,
"outDir": "./dist"
}
}
-
При першій компіляції створюється tsconfig.tsbuildinfo.
-
При наступних компіляціях TS перевіряє, які файли змінилися, і компілює лише їх.
-
Проєкт з 1000 файлів.
-
Змінено тільки 2 файли.
-
При incremental: true компілятор згенерує JS тільки для цих 2 файлів і оновить .tsbuildinfo.
-
composite: true
— потрібен для проєктів з references (Project References). -
tsBuildInfoFile
— можна задати власне ім’я та шлях для .tsbuildinfo.
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.cache/tsbuildinfo"
}
}
-
Значне скорочення часу компіляції на великих проєктах.
-
Сумісність із Project References для багатомодульних систем.
-
Підтримка у CLI (tsc --build) та IDE.
Поступова збірка дозволяє компілювати лише змінені файли, прискорюючи процес розробки та інтеграцію з багатомодульними проєктами.
58. Що робить параметр компілятора noImplicitAny у TypeScript і як він працює?
noImplicitAny
— це параметр компілятора, який забороняє TypeScript автоматично
підставляти тип any там, де він не вказаний явно.
-
Якщо компілятор не може вивести тип і немає явного типу, він видає помилку.
-
Це допомагає робити код більш типізованим і безпечним.
{
"compilerOptions": {
"noImplicitAny": true
}
}
function add(a, b) {
return a + b;
}
-
a
таb
автоматично отримують типany
. -
TypeScript не видає помилку, але типізація відсутня.
function add(a, b) {
return a + b;
}
// ❌ Помилка: Parameter 'a' implicitly has an 'any' type
- Тепер потрібно явно вказати типи:
function add(a: number, b: number): number {
return a + b;
}
[1, 2, 3].map((x) => x * 2); // Без помилки, тип виведено
- Тут TypeScript може вивести тип (number), тому помилка не з’являється.
-
noImplicitAny: true = сувора політика типів.
-
Запобігає неявному any, роблячи код більш безпечним.
-
Рекомендується завжди увімкати у проєктах, особливо для великих команд.
59. Як увімкнути суворі перевірки на null та undefined у TypeScript?
Сувора перевірка на null
і undefined
допомагає уникати помилок типу
Cannot read property of undefined
.
-
Параметр strictNullChecks змушує TypeScript розрізняти типи null та undefined від інших типів.
-
Без нього всі типи за замовчуванням можуть бути null/undefined.
{
"compilerOptions": {
"strictNullChecks": true
}
}
- Альтернатива: увімкнути весь "strict": true — включає strictNullChecks та інші суворі опції.
let name: string = "Viktor";
name = null; // ❌ Помилка: Type 'null' is not assignable to type 'string'
let age: number | null = null; // ✅ Допустимо
function greet(name: string | null) {
if (name !== null) {
console.log("Hello " + name);
}
}
greet(null); // Коректно
greet("Alice"); // Коректно
- Тепер TypeScript змушує обробляти можливий null, знижуючи ризик runtime-помилок.
-
strictNullChecks: true → сувора перевірка null/undefined.
-
Рекомендується для безпечнішого і передбачуваного коду.
-
Часто використовується разом із "strict": true для максимальної суворості.
60. Як у TypeScript керувати визначеннями типів (.d.ts) для сторонніх бібліотек?
- Бібліотека має власні типи
- Деякі пакети вже містять
.d.ts
у собі (наприклад,axios
,react
).
import axios from "axios";
axios.get<string>("https://api.test"); // працює, бо типи вбудовані
- Використання DefinitelyTyped
- Якщо типів немає в пакеті → інсталюємо окремо:
npm install --save-dev @types/lodash
- Після цього TS автоматично підхоплює типи.
- Створення власних визначень
Якщо немає офіційних або community-типів:
- Створюємо файл
*.d.ts
, наприкладcustom.d.ts
:
declare module "legacy-lib" {
export function doSomething(input: string): number;
}
- Тепер можна імпортувати:
import { doSomething } from "legacy-lib";
- Тип any як fallback
Якщо типи зовсім невідомі:
declare module "unknown-lib";
-
Усі імпорти з цього модуля будуть типу any.
-
Це останній варіант, коли немає часу писати типи.
- Конфігурація пошуку типів
У tsconfig.json
можна вказати, де брати типи:
{
"compilerOptions": {
"typeRoots": ["./types", "./node_modules/@types"]
}
}
typeRoots
→ де шукати визначення.
types
→ обмежити список типів, які підключаються.
-
Використовуємо вбудовані типи пакета.
-
Якщо їх немає → ставимо
@types/…
. -
Якщо і там немає → пишемо власні
.d.ts
. -
Як fallback →
any
.
61. Що таке DefinitelyTyped і яке його відношення до TypeScript?
DefinitelyTyped — це великий open-source репозиторій GitHub, у якому
зберігаються файли визначень типів (.d.ts
) для сторонніх JavaScript-бібліотек,
які самі по собі не містять типів.
-
URL: https://github.com/DefinitelyTyped/DefinitelyTyped
-
Публікується у вигляді npm-пакетів у просторі імен
@types
.
-
Хтось пише бібліотеку на JS без TypeScript.
-
Спільнота додає до DefinitelyTyped файл
index.d.ts
, який описує API цієї бібліотеки. -
Ви встановлюєте типи через npm:
npm install --save-dev @types/lodash
- TypeScript автоматично знаходить типи й дозволяє безпечно працювати з бібліотекою:
import _ from "lodash";
const nums: number[] = [1, 2, 3];
const doubled = _.map(nums, x => x * 2); // ✅ коректні типи
-
TypeScript не постачається з типами для всіх JS-бібліотек.
-
DefinitelyTyped заповнює цю прогалину → робить будь-яку популярну JS-бібліотеку "типобезпечною".
-
Визначення типів автоматично інтегруються з TS без додаткових налаштувань.
-
DefinitelyTyped
= репозиторій типів для JS-бібліотек. -
Публікується як пакети
@types/*
. -
Дозволяє використовувати бібліотеки без вбудованих типів у TypeScript із повноцінною підтримкою IntelliSense і перевіркою типів.
62. Як у TypeScript використовувати файли типів (@types) з npm?
- Якщо бібліотека має вбудовані типи
Багато сучасних пакетів (наприклад, axios, react) уже містять index.d.ts у собі. Додатково нічого ставити не треба:
import axios from "axios";
axios.get<string>("https://api.test");
- Якщо бібліотека не має типів
Тоді шукаємо їх у npm-namespace @types:
npm install --save-dev @types/lodash
Тепер у коді:
import _ from "lodash";
const result = _.chunk([1,2,3,4], 2); // тип: number[][]
- Якщо немає готових типів у @types
Створюємо власний файл декларацій, наприклад src/types/custom-lib.d.ts:
declare module "custom-lib" {
export function doSomething(x: string): number;
}
TypeScript автоматично підхопить ці типи (якщо шлях входить у typeRoots або в include у tsconfig.json).
- Керування типами через tsconfig.json
За замовчуванням TS підтягує всі типи з node_modules/@types.
Можна обмежити:
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
→ тоді інші типи ігноруються.
-
Вбудовані типи → просто імпортуємо.
-
Через @types → ставимо npm install @types/....
-
Власні типи → створюємо .d.ts.
-
Контроль підключення → через tsconfig.json.
63. Як інтегрувати TypeScript у проєкт Angular?
- Angular побудований поверх TypeScript
-
Починаючи з Angular 2+, офіційний фреймворк завжди працює з TS як основною мовою.
-
Angular CLI автоматично генерує tsconfig.json з оптимальними параметрами.
- Створення проєкту
npm install -g @angular/cli
ng new my-app
→ CLI налаштує TypeScript, компіляцію та структуру файлів.
- Конфігурація TypeScript
У корені буде tsconfig.json
, наприклад:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
experimentalDecorators
+emitDecoratorMetadata
→ потрібні для декораторів Angular (@Component
,@Injectable
тощо).
- Використання TypeScript у коді Angular
- Класи компонентів і сервісів пишуться на TS:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
title: string = 'My Angular App';
}
- Використовуються інтерфейси, дженерики, enum’и для моделей даних.
- Типізація сервісів і HTTP-запитів
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
-
Angular = нативна інтеграція з TypeScript.
-
Angular CLI автоматично налаштовує TS.
-
TS використовується у компонентах, сервісах, DI, шаблонній перевірці.
-
Основні параметри: strict, experimentalDecorators, emitDecoratorMetadata.
64. Як інтегрувати TypeScript у React-проєкт?
- Створення проєкту з TypeScript
- Використати Next.js:
npx create-next-app@latest
- Або Vite:
npm create vite@latest my-app -- --template react-ts
- Конфігурація
tsconfig.json
Приклад базових налаштувань:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
- Типізація компонентів
- Функціональний компонент:
type Props = { title: string; count?: number };
const Header: React.FC<Props> = ({ title, count }) => (
<h1>{title} {count}</h1>
);
- З children:
type Props = { children: React.ReactNode };
const Layout = ({ children }: Props) => <div>{children}</div>;
- Хуки з дженериками
const [items, setItems] = useState<string[]>([]);
const ref = useRef<HTMLInputElement>(null);
- Типізація подій
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
- Типи для API / props drilling
Використовуються інтерфейси та типи для моделей даних:
interface User {
id: number;
name: string;
}
const UserCard = ({ user }: { user: User }) => <p>{user.name}</p>;
-
React інтегрується з TS через шаблони Next.js або Vite.
-
tsconfig.json
маєjsx: react-jsx
. -
Компоненти, хуки та події типізуються через інтерфейси, дженерики та утиліти React (
React.FC
,React.ReactNode
). -
Це підвищує безпеку коду та зручність роботи з пропсами й стейтом.
65. Як інтегрувати TypeScript у Vue.js-проєкт?
- Створення Vue + TypeScript проєкту
Зараз офіційний спосіб — Vite + Vue CLI (create-vue):
npm create vue@latest
Під час створення проєкту обираєте Add TypeScript? -> Yes.
- Використання
.vue
з TypeScript
У Vue 3 <script setup> підтримує TypeScript напряму:
<script setup lang="ts">
import { ref } from "vue"
const count = ref<number>(0)
function increment(step: number): void {
count.value += step
}
</script>
<template>
<button @click="increment(1)">
Count: {{ count }}
</button>
</template>
- Типізація пропсів і емісій
<script setup lang="ts">
interface Props {
msg: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: "update", value: number): void
}>()
</script>
- Типи для композиційного API
import { Ref } from "vue"
function useCounter(initial: number): { count: Ref<number>, inc: () => void } {
const count = ref(initial)
const inc = () => count.value++
return { count, inc }
}
- Конфігурація
tsconfig.json
Важливі опції:
{
"compilerOptions": {
"strict": true,
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Node",
"jsx": "preserve",
"allowJs": false,
"types": ["vite/client"]
}
}
- Додаткові пакети
-
vue-tsc
— статична перевірка .vue файлів -
@vue/runtime-core
— типи Vue API -
volar
(IDE extension) — краща інтеграція TS у Vue
У Vue 2 можна було додавати TypeScript через vue-class-component
, але зараз
рекомендується Vue 3 + <script setup lang="ts">
— це найпростіший і нативний
варіант.
66. Як використовувати TypeScript у Node.js-додатках?
- Ініціалізація проєкту
mkdir my-node-ts-app && cd my-node-ts-app
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init
У tsconfig.json для Node.js найчастіше:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS", // або "ESNext" для ESM
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
- Структура проєкту
src/
├─ index.ts
└─ utils.ts
- Приклад коду з типами
// src/index.ts
import { readFileSync } from "fs"
function readConfig(path: string): Record<string, unknown> {
const content = readFileSync(path, "utf-8")
return JSON.parse(content)
}
console.log(readConfig("./config.json"))
- Запуск
Прямо через ts-node (зручно для девелопменту):
npx ts-node src/index.ts
З компіляцією в JS (продакшн):
npx tsc
node dist/index.js
- Використання з Express
npm install express
npm install @types/express --save-dev
import express, { Request, Response } from "express"
const app = express()
app.get("/", (req: Request, res: Response) => {
res.send("Hello, TS + Node.js")
})
app.listen(3000, () => console.log("Server running"))
- Переваги TypeScript у Node.js
-
Безпечні типи для API (Express, FS, DB).
-
Зручні автопідказки в IDE.
-
Краща підтримка великих кодових баз.
-
Легше уникати runtime-помилок.
67. Які переваги використання статичного аналізатора коду (TSLint або ESLint з TypeScript-плагіном)?
Переваги:
-
Єдиний кодстайл — забезпечує консистентність у команді (наприклад, лапки ' vs ").
-
Раннє виявлення помилок — ловить некоректні конструкції ще до запуску (невикористані змінні, неправильні імпорти).
-
Краща читабельність — автоматичне форматування та правила полегшують підтримку коду.
-
Інтеграція з IDE/CI — помилки видно під час розробки, а також можна блокувати комміти/білди з помилками.
-
Безпека — правила допомагають уникати небезпечних практик (наприклад, any або eval).
-
Автоматичні виправлення — більшість проблем виправляється командою eslint --fix.
68. Як налаштувати збірку проєкту на TypeScript за допомогою Webpack?
Крок 1. Встановлення залежностей
npm install --save-dev webpack webpack-cli webpack-dev-server \
typescript ts-loader source-map-loader \
html-webpack-plugin clean-webpack-plugin
Крок 2. Ініціалізація TypeScript
npx tsc --init
У tsconfig.json
важливо:
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"sourceMap": true,
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
Крок 3. Структура проєкту
project/
├─ src/
│ └─ index.ts
├─ dist/
├─ tsconfig.json
└─ webpack.config.js
Крок 4. Налаштування Webpack
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.ts$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
},
],
},
resolve: {
extensions: [".ts", ".js"],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
devtool: "source-map",
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "src/index.html",
}),
],
devServer: {
static: "./dist",
hot: true,
port: 3000,
},
};
Крок 5. Сценарії npm
"scripts": {
"build": "webpack --mode production",
"start": "webpack serve --mode development"
}
В результаті:
-
npm start
запускає dev-server з TypeScript і hot reload. -
npm run build
створює оптимізований бандл у dist/.
69. Як інтегрувати TypeScript з Babel і в чому різниця від використання tsc?
-
TypeScript Compiler (tsc): компілює TS → JS і перевіряє типи.
-
Babel: трансформує TS-синтаксис → JS, але ігнорує типи (типи видаляються на етапі компіляції).
Тобто:
-
Якщо потрібна лише підтримка синтаксису (наприклад, у React), достатньо Babel.
-
Якщо потрібна перевірка типів, треба додатково запускати tsc --noEmit або використовувати fork-ts-checker-webpack-plugin.
- Залежності
npm install --save-dev @babel/core @babel/preset-env @babel/preset-typescript \
@babel/preset-react
.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
]
}
package.json
scripts
"scripts": {
"build": "babel src --extensions \".ts,.tsx\" --out-dir dist",
"type-check": "tsc --noEmit"
}
-
Проєкт на React з сучасними фічами, які Babel трансформує краще за tsc.
-
Потрібна інтеграція з Webpack/Rollup/Vite.
-
Хочеш швидшу збірку (Babel швидший, але без type-check).
-
Якщо потрібен чистий TS-проєкт без зайвих тулів.
-
Якщо важлива строга перевірка типів у компіляції.
-
Babel робить трансформацію коду.
-
tsc --noEmit або fork-ts-checker-webpack-plugin робить перевірку типів.
70. Як TypeScript інтегрується з Visual Studio Code та іншими IDE?
-
Вбудована підтримка: VS Code вже має TypeScript Language Service.
-
Автодоповнення (IntelliSense): пропонує методи, змінні, типи в реальному часі.
-
Навігація по коду: Go to Definition, Peek Definition, Find References.
-
Перевірка типів на льоту: IDE показує помилки ще до компіляції.
-
Refactoring tools: перейменування змінних, витягування інтерфейсів, зміна сигнатур функцій.
-
Source Maps інтеграція: зручний дебаг у браузері/Node.js.
-
tsconfig.json awareness: VS Code читає конфігурацію проєкту й підлаштовує підсвічування.
-
Використовують TypeScript Language Server (tsserver).
-
Дають аналогічний набір можливостей: IntelliSense, типізація, refactor, linting.
-
Перевага WebStorm/IntelliJ — вбудовані інструменти для Angular/React, але VS Code легший і гнучкіший через плагіни.
✅ Висновок:
-
TypeScript максимально ефективний у VS Code, бо Microsoft розробляє і сам TypeScript, і VS Code.
-
У інших IDE можливості теж доступні, але VS Code зазвичай оновлюється швидше й краще інтегрований.
71. Які best practices для організації коду та структурування TypeScript-застосунку?
Основні принципи структурування
- Використовуй tsconfig.json для централізованих налаштувань
-
strict: true → строгі перевірки.
-
baseUrl + paths → заміна відносних імпортів (../../../) на зрозумілі alias-и.
- Організація папок
src/
components/ // UI або бізнес-компоненти
services/ // API, робота з даними
models/ // інтерфейси та типи
utils/ // хелпери
hooks/ // кастомні React hooks
config/ // налаштування
index.ts // точка входу
- Кожен модуль експортує лише те, що потрібно (через index.ts у папці).
- Використовуй інтерфейси/типи для контрактів
-
interface для моделей даних та API.
-
type для Union/Intersection.
- Сервіси й утиліти мають бути незалежні від UI
- Розділяй бізнес-логіку (services, models) і UI-логіку (components).
- Використовуй Barrel exports (index.ts)
// src/services/index.ts
export * from './authService';
export * from './userService';
- Розділяй типи в окрему папку types/
-
Добре для спільних моделей (User, Product).
-
Використовуй @types із npm, якщо є.
- Використовуй ESLint + Prettier з правилами для TS
-
Забезпечує єдиний стиль коду.
-
Ловить помилки раніше компіляції.
- Type-safe API-клієнти
-
Використовуй axios/fetch з власними типами респонсів.
-
Винось DTO (Data Transfer Objects) окремо.
- Ніколи не вимикай строгі прапорці
- noImplicitAny, strictNullChecks, noUnusedLocals.
- Використовуй async/await з типами
- Обгортай відповіді від API у Promise.
✅ Висновок:
- Правильна структура TS-проєкту = чітке розділення шарів (UI / бізнес / дані), строга типізація, єдиний стиль коду, мінімізація "any".
72. Як правильно використовувати async/await у TypeScript?
Основи
-
async
→ функція завжди повертаєPromise<T>
. -
await
→ зупиняє виконання до завершенняPromise
.
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json() as Promise<User>;
}
Обробка помилок
Використовуй try/catch
:
async function safeFetch(): Promise<void> {
try {
const data = await fetchUser(1);
console.log(data);
} catch (err: unknown) {
if (err instanceof Error) {
console.error(err.message);
}
}
}
Паралельні виклики
async function loadData() {
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
}
Типізація результатів
Функція повинна мати чіткий тип повернення:
async function getNumber(): Promise<number> {
return 42;
}
✅ Висновок:
-
У TypeScript
async/await
= асинхронний код із чіткою типізацією результатів. -
Завжди використовуй
try/catch
, або обробку через.catch()
приPromise.all
.
73. Які підходи до керування станом у TypeScript-застосунках і як їх правильно типізувати?
Локальний стан (React / Vue / Angular)
- Використовуй узагальнені хуки/сервіси з чіткими типами.
const [count, setCount] = useState<number>(0);
Глобальний стан
- Redux Toolkit + TypeScript
-
Типізація RootState, AppDispatch.
-
Використовуй createSlice, щоб уникати boilerplate.
interface CounterState { value: number }
const initialState: CounterState = { value: 0 };
- MobX
-
Класи з observable + інтерфейси.
-
Просте зв’язування з TS через декоратори.
- Context API (React)
- Визначай інтерфейси для значень контексту.
interface AuthContext {
user: User | null;
login: (u: User) => void;
}
Сторонні менеджери стану
-
Zustand → легкий синтаксис + TS дружній.
-
Recoil → атоми зі строгими типами.
-
XState → finite-state machines з автоматичною типізацією переходів.
Best Practices
-
Завжди визначай інтерфейси/типи стану (interface State { ... }).
-
Використовуй enum для action type або as const для уникнення помилок у рядках.
-
Використовуй типобезпечні селектори ((state: RootState) => state.user).
-
Мінімізуй глобальний стан → тримай лише дані, які реально потрібні в багатьох місцях.
✅ Висновок:
- TypeScript робить керування станом більш безпечним: ти отримуєш автодоповнення, контроль над структурою стану та уникнення runtime-помилок.
74. Які шаблони проектування найчастіше застосовують у TypeScript?
- Singleton – один екземпляр класу (наприклад, конфігурація, логгер).
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance() {
if (!Logger.instance) Logger.instance = new Logger();
return Logger.instance;
}
}
- Factory Method / Abstract Factory – створення об’єктів через фабрику з типами.
- Adapter – адаптація інтерфейсу під інший.
interface Payment { pay(amount: number): void; }
class PayPal { makePayment(sum: number) { /* ... */ } }
class PayPalAdapter implements Payment {
constructor(private paypal: PayPal) {}
pay(amount: number) { this.paypal.makePayment(amount); }
}
-
Decorator – динамічно додає поведінку до класу.
-
Proxy – контроль доступу або кешування.
- Observer – підписка/відписка (події, реактивність).
type Listener = (data: string) => void;
class EventEmitter {
private listeners: Listener[] = [];
subscribe(l: Listener) { this.listeners.push(l); }
emit(data: string) { this.listeners.forEach(l => l(data)); }
}
-
Strategy – змінюваний алгоритм (наприклад, різні способи валідації).
-
Command – інкапсуляція дії у вигляді об’єкта.
-
Mediator – координація взаємодії між об’єктами.
-
Інтерфейси та дженерики роблять шаблони більш строгими.
-
Decorators (експериментальні) дозволяють реалізовувати Decorator, DI та Aspect-Oriented Programming.
-
Union/Discriminated Unions часто спрощують реалізацію State чи Strategy.
75. Які основні підходи для налагодження TypeScript-застосунків?
- Source Maps
- У
tsconfig.json
додати:
{
"compilerOptions": {
"sourceMap": true
}
}
- Це дозволяє дебагеру (Chrome DevTools, VS Code) показувати оригінальний .ts код замість згенерованого .js.
- Debug у VS Code
- Налаштувати
launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug TS",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
}
- Ставиш брейкпоінти прямо у TypeScript-файлах.
- Debug у браузері
-
Використовувати
webpack
абоvite
з підтримкою source maps. -
Відкрити Chrome DevTools → вкладка Sources → знаходиш .ts файл → ставиш брейкпоінт.
- Консольні інструменти
console.log
,console.table
,debugger
працюють так само, але з відображенням через source maps.
- Node.js Debugging
- Запуск з Node.js:
node --inspect-brk dist/index.js
- Підключаєшся через Chrome DevTools або VS Code.
- Unit-тести з дебагом
-
Якщо використовуєш Jest / Mocha → можна запускати з
--inspect
. -
Це допомагає відслідковувати помилки ще до рантайму.
✅ Ключ: завжди компілюй з sourceMap
і налагоджуй оригінальний .ts
код, а не
згенерований .js
.
76. Як писати модульні тести для TypeScript-коду?
- Вибір тестового фреймворку
-
Найчастіше: Jest, Mocha + Chai, Vitest.
-
Для фронтенду (React/Angular): зазвичай Jest.
- Налаштування TypeScript
- Встановлення залежностей (на прикладі Jest):
npm install --save-dev jest ts-jest @types/jest
- Ініціалізація:
npx ts-jest config:init
- Приклад функції (яку тестуємо):
// src/utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
- Приклад тесту
// tests/math.test.ts
import { add } from "../src/utils/math";
test("adds two numbers", () => {
expect(add(2, 3)).toBe(5);
});
- Запуск тестів
npx jest
- Типізація у тестах
-
Використовуєш ті самі TS-типи.
-
Якщо треба mock-и →
jest.mock
абоts-mockito
.
- Кращі практики
-
Тести зберігати в
__tests__/
або*.test.ts
. -
Перевіряти і позитивні, і негативні кейси.
-
Використовувати beforeEach / afterEach для підготовки оточення.
-
Перекривати тільки те, що потрібно (не мокати все без потреби).
✅ Резюме: модульні тести в TypeScript пишуться так само, як у JavaScript, але з додатковими перевагами типізації. Найзручніше — Jest + ts-jest.
77. Які тестові фреймворки зазвичай застосовують для TypeScript-проєктів?
Найпопулярніші
- Jest
-
Найпоширеніший для фронтенду та Node.js.
-
Підтримує TypeScript через ts-jest.
-
Вбудовані мокі, асинхронні тести, snapshot testing.
- Vitest
-
Швидкий, сучасний, інтегрується з Vite.
-
Підтримує TS нативно.
-
API сумісний із Jest.
- Mocha + Chai
-
Класичний стек для Node.js.
-
ts-node дозволяє запускати тести на TS без компіляції.
-
Чудово підходить для серверних застосунків.
- AVA
-
Мінімалістичний, паралельне виконання тестів.
-
Підтримка TS через ts-node/register.
- Jasmine
-
Використовувався для Angular, можна писати тести на TS.
-
Менш популярний сьогодні через Jest/Vitest.
78. Як виконувати наскрізне (E2E) тестування у TypeScript-застосунках?
- Cypress
-
Дуже популярний для фронтенду.
-
Підтримує TypeScript через tsconfig.json.
-
Вбудований девтулс, мок-сервер, зручні API для селекторів.
- Playwright
-
Кросбраузерне тестування (Chromium, Firefox, WebKit).
-
TypeScript підтримується "з коробки".
-
Можна тестувати UI, API, мобільні емуляції.
- Puppeteer
-
Автоматизація браузера Chrome.
-
TypeScript через npm типи (@types/puppeteer).
- tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"types": ["cypress", "node"]
},
"include": ["cypress/**/*.ts"]
}
- Cypress приклад тесту на TS
describe('Login page', () => {
it('should log in successfully', () => {
cy.visit('/login');
cy.get('input[name="email"]').type('[email protected]');
cy.get('input[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
});
- Playwright приклад
import { test, expect } from '@playwright/test';
test('login test', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('input[name=email]', '[email protected]');
await page.fill('input[name=password]', 'password123');
await page.click('button[type=submit]');
await expect(page).toHaveURL(/dashboard/);
});
-
Типізувати тести і селектори через інтерфейси або константи.
-
Використовувати Page Object Model (POM) для організації коду тестів.
-
Писати окремі тести для функціональних сценаріїв, а не перевіряти все одним тестом.
-
Інтегрувати E2E у CI/CD (GitHub Actions, GitLab CI) з відловом помилок.
✅ Висновок: TypeScript + E2E тести дозволяють мати типобезпечні, надійні та підтримувані тести для UI і API.
-
Cypress → швидкий для фронтенду.
-
Playwright → кросбраузерна автоматизація.