Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

TypeScript: питання на співбесіді рівня Junior

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

36 питань

Для опису форми об'єкта обидва працюють майже однаково:

interface User {
  id: number;
  name: string;
}

type UserType = {
  id: number;
  name: string;
};

Відмінності:

  • type описує будь-що, не лише об'єкти: об'єднання (type Status = 'draft' | 'published'), кортежі, примітиви, умовні й зіставлені типи. interface - лише форму об'єкта (і функції, класу).
  • Злиття оголошень: два interface з однаковим іменем зливаються в один. Так розширюють чужі типи - наприклад, додають поле до Window. type з тим самим іменем - помилка.
  • Розширення: interface Admin extends User {} проти type Admin = User & { role: string }. При конфлікті полів extends одразу дає зрозумілу помилку, а перетин & може мовчки дати never.
  • Повідомлення про помилки з інтерфейсами інколи читабельніші, а великі ієрархії інтерфейсів компілятор перевіряє трохи швидше.

Практичне правило: interface - для форм об'єктів і публічних API (їх можна розширити), type - для об'єднань, кортежів і всього обчислюваного. Головне - послідовність у кодовій базі: обидва варіанти правильні.

Докладніше в документації: Аліаси типів та інтерфейси

Обидва означають «тут може бути будь-яке значення», але з протилежним ставленням до перевірок:

  • any вимикає перевірку типів. З ним можна робити що завгодно - викликати методи, звертатися до полів, передавати куди завгодно. Компілятор мовчить, а помилка вилізе під час виконання.
  • unknown - безпечний аналог: присвоїти в нього можна будь-що, але використати значення не можна, доки не доведете, що воно потрібного типу.
const a: any = JSON.parse(text);
a.user.name.toUpperCase();     // компілюється - і може впасти

const u: unknown = JSON.parse(text);
u.user;                        // помилка компіляції

if (typeof u === 'object' && u !== null && 'user' in u) {
  // тут TypeScript знає більше
}

Чому any шкідливий: він «заразний». Значення, отримане з any, теж any, і відсутність перевірок непомітно розповзається кодом.

Коли що:

  • unknown - для даних ззовні: JSON.parse, відповідь API, catch (error) (у строгому режимі error і так unknown), значення з localStorage. Звужуєте перевірками або схемою валідації (Zod).
  • any - як тимчасовий захід при міграції з JavaScript, з коментарем і планом прибрати.

У tsconfig варто тримати "strict": true (він вмикає noImplicitAny), а лінтер налаштувати так, щоб явний any був помітним.

Докладніше в документації: unknown

TypeScript виводить типи (type inference) з ініціалізації, повернених значень і контексту. Анотації потрібні не всюди - надлишкові лише засмічують код.

const title = 'Laravel Ukraine';            // string - виведено
const ids = [1, 2, 3];                      // number[]
const user = { id: 7, name: 'Оля' };        // { id: number; name: string }

function total(prices: number[]) {          // параметри - анотувати
  return prices.reduce((sum, p) => sum + p, 0);   // повернення number - виведено
}

Де анотація обов'язкова або корисна:

  • параметри функцій - TypeScript не знає, що в них передадуть (виняток - колбеки, де тип відомий з контексту: ids.map((id) => ...));
  • змінна без ініціалізатора: let current: User | null = null; - інакше тип буде лише null;
  • порожні колекції: const errors: string[] = []; - інакше never[] чи any[];
  • публічний API модуля - експортовані функції з явним типом повернення: помилка в реалізації буде помічена в самій функції, а не в місцях виклику, і зміна поведінки не змінить API непомітно;
  • коли виведений тип ширший за потрібний: const status: 'draft' | 'published' = 'draft' - щоб змінна не стала просто string для let.

Основні типи:

  • примітиви: string, number, boolean, bigint, symbol, null, undefined;
  • масиви: string[] або Array<string>;
  • об'єкти: { id: number; email?: string } (? - необов'язкове поле);
  • об'єднання: string | number;
  • функції: (value: string) => boolean.

Що не варто анотувати: очевидні локальні змінні (const count: number = 0) - це шум. Те саме з типом повернення простих внутрішніх функцій.

Корисна перевірка в редакторі: наведення курсору показує виведений тип. Якщо він any чи несподівано широкий - місце для анотації.

strict: true вмикає noImplicitAny: параметр без анотації, тип якого не вдалося вивести, стане помилкою, а не тихим any. З TypeScript 6.0 strict увімкнено за замовчуванням - у старих проєктах його варто перевірити явно в tsconfig.

Докладніше в документації: Повсякденні типи

Задача та сама - обмежити значення фіксованим набором. Підходи різні.

enum - конструкція TypeScript, яка генерує JavaScript-код (об'єкт під час виконання):

enum Status {
  Draft = 'draft',
  Published = 'published',
}

function publish(status: Status) {}
publish(Status.Published);
publish('published');   // помилка: рядок не є Status

Об'єднання літеральних типів - лише тип, після компіляції зникає повністю:

type Status = 'draft' | 'published';

function publish(status: Status) {}
publish('published');   // ок

Аргументи на користь об'єднання:

  • нуль коду під час виконання - тип стирається;
  • сумісність зі звичайними рядками: значення з API ('published') одразу підходять, без перетворень;
  • сумісність з інструментами, що лише стирають типи: Node.js із вбудованим запуском TypeScript, erasableSyntaxOnly, швидкі транспілятори. enum - не «стиральний» синтаксис: його треба перетворювати на код, і з erasableSyntaxOnly компілятор його забороняє;
  • числові enum мають історичні дивацтва: зворотне відображення (Status[0] === 'Draft'), а у старих версіях TypeScript дозволяв присвоїти будь-яке число. З TS 5.0 присвоєння числа поза значеннями enum - помилка.

Коли потрібен ще й список значень (для <select>, валідації) - об'єкт з as const:

const STATUSES = ['draft', 'published', 'archived'] as const;
type Status = (typeof STATUSES)[number];   // 'draft' | 'published' | 'archived'

STATUSES.includes(value);   // перевірка під час виконання

Або об'єкт-«перелік»:

const Status = { Draft: 'draft', Published: 'published' } as const;
type Status = (typeof Status)[keyof typeof Status];

Один і той самий ідентифікатор - і значення, і тип: використання як в enum, але без генерації коду.

const enum вбудовує значення під час компіляції, але не працює з ізольованою транспіляцією (кожен файл окремо - Vite, esbuild) і має проблеми з бібліотеками.

Практична рекомендація: для нового коду - об'єднання літералів або as const-об'єкти. enum - якщо так прийнято в проєкті чи потрібна сумісність з наявним кодом.

Зв'язок з Laravel: PHP-енуми (enum Status: string) зручно генерувати у TypeScript як об'єднання літералів - значення збігаються з тими, що приходять у JSON.

Докладніше в документації: Enums

Без strictNullChecks значення null і undefined можна присвоїти будь-якому типу - і TypeScript не попереджає про найчастішу помилку JavaScript: Cannot read properties of undefined.

З strictNullChecks (входить у strict: true) null і undefined - окремі типи, і їх треба явно дозволити:

let name: string = null;            // помилка
let middleName: string | null = null;   // ок

function findUser(id: number): User | undefined {
  return users.find((u) => u.id === id);
}

const user = findUser(5);
user.name;                          // помилка: user може бути undefined

Як працювати з можливою відсутністю значення:

// перевірка - TypeScript звужує тип
if (user) {
  user.name;                        // User
}

// опціональний ланцюжок
const city = user?.address?.city;   // string | undefined

// значення за замовчуванням
const displayName = user?.name ?? 'Гість';

// ранній вихід
if (!user) throw new Error('Користувача не знайдено');
user.name;                          // далі - User

Необов'язкові властивості (email?: string) мають тип string | undefined. Перед використанням - перевірка.

null чи undefined? У JavaScript обидва означають «немає значення»:

  • undefined - «не задано» (необов'язкові параметри, відсутні поля, результат find);
  • null - «навмисно порожньо» (часто з бази й API: Laravel серіалізує NULL колонки як null у JSON).

Корисно домовитися в проєкті: наприклад, undefined у власному коді, а null - там, де так приходить з сервера.

Оператор ! (non-null assertion) - user!.name - каже компілятору «повір, тут не null». Перевірки під час виконання немає: якщо твердження хибне, помилка повернеться. Використовувати лише там, де гарантію справді дає щось поза системою типів, і краще з коментарем.

Чому це найцінніша опція strict: вона змушує обробити випадки «не знайдено», «ще не завантажено», «поле не заповнене» - саме там, де найчастіше падають застосунки.

Докладніше в документації: null і undefined

string[] і Array<string> - одне й те саме, два записи одного типу. Короткий запис зазвичай читається простіше; узагальнений зручніший для складних типів елементів:

const tags: string[] = ['php', 'vue'];
const handlers: Array<() => void> = [];   // замість (() => void)[]

Readonly-масиви забороняють змінювати масив через це посилання:

function sum(values: readonly number[]): number {
  values.push(1);      // помилка: push немає в readonly number[]
  values[0] = 5;       // помилка
  return values.reduce((a, b) => a + b, 0);   // читання - можна
}

readonly number[] і ReadonlyArray<number> - знову два записи одного типу. Методи, що змінюють масив (push, pop, splice, sort, reverse), у ньому відсутні; немутуючі (map, filter, toSorted) - доступні.

Навіщо:

  • контракт функції: параметр readonly T[] повідомляє, що функція масив не змінить. Викликач може спокійно передати свій стан;
  • ширша сумісність: у readonly number[] можна передати і звичайний масив, і readonly. Функція з параметром number[] не прийме readonly-масив. Тому для параметрів, які лише читаються, readonly - кращий вибір;
  • стан у React/Vue/Redux: заборона випадкової мутації на рівні типів.

as const робить масив літералів readonly-кортежем:

const sizes = ['sm', 'md', 'lg'] as const;   // readonly ['sm', 'md', 'lg']
type Size = (typeof sizes)[number];          // 'sm' | 'md' | 'lg'

Обмеження:

  • лише на рівні типів: під час виконання це звичайний масив. Хтось із посиланням number[] чи через as може його змінити. Для справжньої незмінності - Object.freeze;
  • поверхнево: readonly User[] забороняє змінювати масив, але не об'єкти в ньому - users[0].name = 'x' пройде. Для глибокої незмінності - ReadonlyArray<Readonly<User>> або власний тип DeepReadonly;
  • присвоєння readonly-масиву в змінну типу T[] - помилка; інколи це змушує додавати readonly у сигнатури по всьому ланцюжку викликів (і це правильно).

Докладніше в документації: Тип ReadonlyArray

keyof T - тип-об'єднання всіх ключів типу T:

interface User {
  id: number;
  name: string;
  email: string;
}

type UserKey = keyof User;   // 'id' | 'name' | 'email'

Головне застосування - функції, що працюють з ключами об'єкта безпечно:

function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: 'Оля', email: 'olia@example.com' };

getField(user, 'name');    // string
getField(user, 'id');      // number
getField(user, 'phone');   // помилка: 'phone' немає в keyof User

Тип повернення T[K] точно відповідає переданому ключу - не string | number, а саме тип цього поля.

Інші застосування:

  • сортування й групування таблиць за назвою колонки: sortBy<T>(items: T[], key: keyof T);
  • перелік дозволених полів для фільтрів, форм, експорту;
  • основа mapped types: { [K in keyof T]: ... }.

Особливості:

  • з індексною сигнатурою keyof дає ширший тип. Для { [key: string]: number } результат - string | number, бо в JavaScript числові ключі об'єкта перетворюються на рядки (obj[1] - те саме, що obj['1']);
  • для масивів keyof string[] містить number і назви методів масиву ('length' | 'push' | ...);
  • лише ключі типу, а не значення: keyof працює з типами. Щоб отримати ключі значення, потрібне keyof typeof config.

Пастка з Object.keys: Object.keys(user) повертає string[], а не (keyof User)[]. Це навмисно: через структурну типізацію в об'єкті під час виконання можуть бути й інші поля, яких немає в типі. Тому for (const key of Object.keys(user)) user[key] - помилка типу, і доводиться або звужувати ключ, або використовувати Object.entries.

Докладніше в документації: Оператор keyof

У позиції типу typeof повертає тип значення (а не рядок 'object', як у JavaScript під час виконання):

const config = {
  apiUrl: '/api',
  retries: 3,
  features: { chat: true },
};

type Config = typeof config;
// { apiUrl: string; retries: number; features: { chat: boolean } }

Навіщо: значення стає джерелом правди, а тип - похідним. Додали поле в об'єкт - тип оновився сам, дублювати опис не треба.

Типові застосування:

1. Тип функції чи її частин:

function createUser(name: string, role: 'admin' | 'editor') {
  return { id: crypto.randomUUID(), name, role, createdAt: new Date() };
}

type User = ReturnType<typeof createUser>;
type CreateUserArgs = Parameters<typeof createUser>;

2. Перелік значень з масиву чи об'єкта:

const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number];   // 'admin' | 'editor' | 'viewer'

const STATUS = { draft: 'Чернетка', published: 'Опубліковано' } as const;
type StatusKey = keyof typeof STATUS;   // 'draft' | 'published'

3. Тип схеми валідації: z.infer<typeof userSchema> - тип з об'єкта Zod.

4. Тип модуля чи класу: typeof import('./api'), typeof User (конструктор, а не екземпляр).

Обмеження:

  • typeof у позиції типу приймає лише ідентифікатор чи доступ до властивості (typeof config.features), але не довільний вираз: typeof fn() - помилка. Для результату виклику - ReturnType<typeof fn>;
  • розширення типів: без as const рядки й числа в об'єкті виводяться як string, number. Якщо потрібні точні літерали - as const.

Обидва typeof в одному рядку:

if (typeof value === 'string') {}   // typeof JavaScript - перевірка під час виконання
type T = typeof value;             // typeof TypeScript - тип під час компіляції

Це різні оператори з однаковою назвою: перший працює в коді, другий - лише в позиції типу.

Докладніше в документації: Оператор typeof для типів

Індексований тип доступу - отримати тип властивості іншого типу тим самим синтаксисом, що й доступ до поля об'єкта:

interface Order {
  id: number;
  status: 'new' | 'paid' | 'shipped';
  customer: { name: string; email: string };
  items: { sku: string; qty: number }[];
}

type OrderStatus = Order['status'];            // 'new' | 'paid' | 'shipped'
type Customer = Order['customer'];             // { name: string; email: string }
type CustomerEmail = Order['customer']['email'];   // string
type IdOrStatus = Order['id' | 'status'];      // number | 'new' | 'paid' | 'shipped'

T[number] - тип елемента масиву:

type OrderItem = Order['items'][number];       // { sku: string; qty: number }

const SIZES = ['sm', 'md', 'lg'] as const;
type Size = (typeof SIZES)[number];            // 'sm' | 'md' | 'lg'

number як індекс означає «будь-який числовий індекс» - тобто тип будь-якого елемента. Для кортежів - об'єднання типів усіх елементів.

Навіщо це:

  • не дублювати описи: тип статусу береться з Order, і якщо статуси зміняться, похідні типи оновляться;
  • типи для частин відповіді API: згенерований тип відповіді великий, а компоненту потрібна одна вкладена частина - ApiResponse['data']['items'][number];
  • поєднання з keyof: T[keyof T] - об'єднання типів усіх значень об'єкта.
const LABELS = { draft: 'Чернетка', published: 'Опубліковано' } as const;
type Label = (typeof LABELS)[keyof typeof LABELS];   // 'Чернетка' | 'Опубліковано'

Обмеження:

  • індекс - тип, а не значення: Order[key], де key - змінна, не працює. Потрібно Order[typeof key] або параметр типу;
  • лише існуючі ключі: Order['phone'] - помилка;
  • необов'язкове поле дає тип з undefined: для email?: string - string | undefined. Прибрати - NonNullable<User['email']>.

Різниця з доступом під час виконання: noUncheckedIndexedAccess додає | undefined до доступу до масиву за індексом у коді, але T[number] у типах повертає тип елемента без undefined.

Докладніше в документації: Indexed access types

Літеральний тип - тип, що допускає одне конкретне значення:

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Code = 200 | 404 | 500;

function request(method: Method, url: string) {}
request('GET', '/api/users');
request('get', '/api/users');   // помилка: 'get' не входить у Method

Об'єднання літералів - основний спосіб описати скінченний набір значень у TypeScript.

Шаблонні рядкові типи (template literal types) будують рядкові типи за шаблоном - як шаблонні рядки JavaScript, але на рівні типів:

type Size = 'sm' | 'md' | 'lg';
type Color = 'red' | 'blue';

type ClassName = `btn-${Size}-${Color}`;
// 'btn-sm-red' | 'btn-sm-blue' | 'btn-md-red' | ... - усі 6 комбінацій

type EventName = `on${Capitalize<'click' | 'focus'>}`;   // 'onClick' | 'onFocus'

type UserRoute = `/users/${number}`;
const ok: UserRoute = '/users/42';
const bad: UserRoute = '/users/abc';   // помилка

Вбудовані типи для рядків: Uppercase, Lowercase, Capitalize, Uncapitalize.

Практичні застосування:

  • ключі подій і перекладів: `auth.${'login' | 'logout'}.title` - помилка в ключі ловиться компілятором;
  • CSS-значення: `${number}px` | `${number}%`;
  • ідентифікатори з префіксом: `usr_${string}`, `ord_${string}` - різні типи ID не сплутаються;
  • разом з mapped types - генерація назв методів (getName, setName) з полів.

Обмеження:

  • комбінаторний вибух: кожна позиція множить варіанти. Об'єднання на десятки тисяч рядків уповільнює компілятор, а понад 100 000 членів - помилка «Expression produces a union type that is too complex to represent»;
  • string у шаблоні (`usr_${string}`) перевіряє лише форму, а не вміст: usr_ з будь-яким продовженням підходить;
  • лише компіляція: рядки з API чи URL під час виконання не перевіряються - потрібна валідація.

Докладніше в документації: Template literal types

Параметр типу може бути обмежений іншим параметром - так описують зв'язок між аргументами функції.

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}

const users = [
  { id: 1, name: 'Оля', age: 28 },
  { id: 2, name: 'Іра', age: 31 },
];

pluck(users, 'name');    // string[]
pluck(users, 'age');     // number[]
pluck(users, 'email');   // помилка: 'email' немає серед ключів

Як це працює:

  1. TypeScript виводить T з першого аргументу - тип елементів масиву;
  2. K extends keyof T дозволяє другим аргументом лише ключі T;
  3. K виводиться як конкретний літерал ('name'), а не весь keyof T, тож тип повернення T[K][] - точний.

Інші приклади зв'язаних параметрів:

// значення має відповідати типу поля
function setField<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value;
}

setField(user, 'age', 30);       // ок
setField(user, 'age', '30');     // помилка: string не number

// групування
function groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]> {
  const groups = new Map<T[K], T[]>();
  for (const item of items) {
    const group = groups.get(item[key]) ?? [];
    group.push(item);
    groups.set(item[key], group);
  }
  return groups;
}

Типові помилки:

  • key: keyof T без окремого параметра: function getField<T>(obj: T, key: keyof T): T[keyof T] - тип повернення стає об'єднанням типів усіх полів. Окремий K зберігає зв'язок з конкретним ключем;
  • K extends string замість keyof T - тоді немає перевірки, що ключ існує;
  • символьні й числові ключі: keyof T може містити number | symbol. Якщо ключ використовується в рядкових операціях (шаблонні рядки), - K extends keyof T & string або Extract<keyof T, string>.

Цей прийом - основа багатьох бібліотек: типізовані таблиці (columnHelper.accessor('email')), форми (register('name') у React Hook Form), сховища стану з селекторами за ключем.

Докладніше в документації: Generics: параметри типу в обмеженнях

Тип функції складається з типів параметрів і типу результату:

function formatPrice(amount: number, currency: string): string {
  return `${amount.toFixed(2)} ${currency}`;
}

// тип функції окремо - для колбеків і змінних
type Formatter = (amount: number, currency: string) => string;
const uah: Formatter = (amount) => `${amount} грн`;   // параметрів може бути менше

Необов'язковий параметр - знак ?. Усередині функції він має тип T | undefined, тож його треба перевіряти:

function greet(name?: string) {
  return `Привіт, ${name ?? 'гостю'}`;
}

Параметр зі значенням за замовчуванням - тип виводиться зі значення, і всередині функції undefined вже не буде:

function paginate(page = 1, perPage = 20) { /* page: number */ }

Rest-параметр збирає решту аргументів у масив чи кортеж:

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

function log(level: 'info' | 'error', ...messages: unknown[]) {}

Що варто знати:

  • необов'язкові параметри йдуть після обов'язкових;
  • name?: string і name: string | undefined - різне: другий обов'язково передати, хай навіть undefined;
  • тип результату можна не писати - TypeScript виведе його. Для публічних функцій (експорт з модуля, бібліотеки) явний тип корисний: помилка в реалізації не змінить мовчки контракт;
  • функція з колбеком меншої арності сумісна - [1, 2].forEach((n) => ...) не вимагає всіх трьох параметрів колбека.

Об'єкт параметрів замість довгого списку - читабельніше й легше розширюється:

function createUser({ name, role = 'user' }: { name: string; role?: 'user' | 'admin' }) {}

Докладніше в документації: Функції: необов'язкові параметри

Ці три типи описують різні ситуації «функція нічого корисного не повертає».

void - результат функції не використовується:

function logEvent(name: string): void {
  console.log(name);
}

Усередині функції з явно вказаним void повертати значення не можна - return 1 дасть помилку.

Але тип функції з void поводиться інакше: функцію, що щось повертає, можна присвоїти змінній типу () => void:

type Callback = () => void;
const cb: Callback = () => 42;   // дозволено

const items: number[] = [];
[1, 2].forEach((n) => items.push(n));   // push повертає number - і це нормально

Це зроблено навмисно: колбек на кшталт forEach ігнорує результат, і вимагати «нічого не повертати» було б незручно. Наслідок - const result = cb() має тип void, а не number: використовувати його не варто.

undefined - функція повертає значення undefined, і викликач може на це покладатися:

function find(id: number): User | undefined {}

Для «нічого не повертає» в типах функцій зазвичай пишуть void, а undefined - коли значення має сенс (T | undefined).

never - функція ніколи не завершується нормально: кидає виняток або нескінченно виконується.

function fail(message: string): never {
  throw new Error(message);
}

function assertNever(value: never): never {
  throw new Error(`Неочікуване значення: ${value}`);
}

TypeScript використовує never в аналізі потоку: код після виклику fail() вважається недосяжним, а в звуженні типу never означає «варіантів не лишилося» - це основа перевірки вичерпності в switch.

Коротко: void - «результат не цікавить», undefined - «повертає саме undefined», never - «не повертає взагалі».

Докладніше в документації: Функції: тип результату void

TypeScript має модифікатори доступу public, protected, private:

  • public (за замовчуванням) - доступно всім;
  • protected - класу й нащадкам;
  • private - лише самому класу.
class Account {
  private balance = 0;
  protected currency = 'UAH';
  #pin = '1234';   // приватне поле JavaScript
}

Ключова різниця - коли діє обмеження.

private TypeScript - лише перевірка компілятора. Після компіляції це звичайна властивість об'єкта: вона видна в Object.keys, у JSON.stringify, доступна з JavaScript-коду. Навіть у TypeScript є лазівка - доступ через квадратні дужки:

const account = new Account();
account.balance;      // помилка компіляції
account['balance'];   // дозволено - навмисна «аварійна дверцята»

#private - справжня приватність під час виконання. Поле недосяжне ззовні класу в принципі: ні через дужки, ні через Object.keys, ні через рефлексію. Спроба звернутися до #pin поза класом - синтаксична помилка JavaScript.

Коли що обирати:

  • #private - коли потрібна гарантія: бібліотеки, код, який використовують з JavaScript, дані, що не повинні потрапити в серіалізацію;
  • private - якщо достатньо перевірки на етапі компіляції й потрібна гнучкість: тести, що зазирають у стан, сумісність зі старим кодом, бібліотеки, яким потрібен доступ до полів (ORM, серіалізатори).

Нюанси #private:

  • #field in obj - перевірка, що об'єкт створено цим класом;
  • приватні поля не працюють через Proxy (Vue reactive, MobX): метод, викликаний на проксі, падає з TypeError;
  • protected аналога в JavaScript не має - лише перевірка TypeScript.

readonly - окремий модифікатор: поле можна присвоїти лише при оголошенні чи в конструкторі. Теж діє тільки під час компіляції.

Докладніше в документації: Класи: видимість членів

Parameter properties - скорочення TypeScript: модифікатор доступу в параметрі конструктора одночасно оголошує поле класу й присвоює йому значення.

class User {
  constructor(
    public readonly id: number,
    private name: string,
  ) {}
}

// те саме, що
class User {
  public readonly id: number;
  private name: string;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

Зручно й коротко - тому цей запис став популярним (Angular, NestJS, багато бекенд-коду на TypeScript).

Проблема: це не «лише типи». Більшість синтаксису TypeScript можна просто стерти - прибрати анотації, і лишиться коректний JavaScript. Parameter properties так не працюють: щоб отримати JavaScript, компілятор має згенерувати код (присвоєння this.id = id).

Те саме з enum (генерує об'єкт), namespace з виконуваним кодом і import x = require().

Чому це стало важливим:

  • Node.js 22.18+/23.6+ виконує .ts файли напряму через «стирання типів» (type stripping) - без компіляції. Синтаксис, що потребує генерації коду, там не підтримується;
  • інструменти на кшталт швидких транспіляторів теж простіше працюють зі «стираним» синтаксисом.

Прапорець erasableSyntaxOnly (TypeScript 5.8+) забороняє такий синтаксис у проєкті - помилка компілятора «This syntax is not allowed when erasableSyntaxOnly is enabled»:

{ "compilerOptions": { "erasableSyntaxOnly": true } }

Чим замінювати:

  • parameter properties - звичайні поля й присвоєння в конструкторі;
  • enum - об'єкт з as const і тип-об'єднання:
const Status = { Draft: 'draft', Published: 'published' } as const;
type Status = (typeof Status)[keyof typeof Status];

Практичний висновок: у новому коді, особливо для Node.js без збирання, варто вмикати erasableSyntaxOnly - тоді TypeScript-код лишається «JavaScript з анотаціями». Parameter properties не помилка, але прив'язують код до компіляції.

Докладніше в документації: Класи: parameter properties

Питання рівня Junior з реальних технічних співбесід - 36 питань у 7 темах, розібраних із відповідями. Нижче - теми цього рівня та сусідні рівні, якщо хочете звузити або розширити підготовку.

Інші рівні
Middle 35 Senior 29

Готуєтесь до співбесіди не просто так: зараз на сайті 7 відкритих вакансій рівня Junior. Переглянути вакансії