Junior: питання на співбесіді з теми «Функції й класи»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Тип функції складається з типів параметрів і типу результату:
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 - «не повертає взагалі».
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(Vuereactive, 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 не помилка, але прив'язують код до компіляції.
extends - успадкування: клас отримує поля й методи батьківського класу (і їхню реалізацію), може їх перевизначити й доповнити.
class Model {
save() { /* загальна логіка збереження */ }
}
class Post extends Model {
title = '';
}
new Post().save(); // метод успадковано
Клас може успадковувати лише один клас.
implements - перевірка відповідності інтерфейсу. Клас обіцяє мати певні члени, але нічого не отримує - реалізацію треба написати самому:
interface Cacheable {
cacheKey(): string;
}
interface Serializable {
toJSON(): object;
}
class Product implements Cacheable, Serializable {
constructor(private id: number) {}
cacheKey() { return `product:${this.id}`; }
toJSON() { return { id: this.id }; }
}
Клас може реалізовувати кілька інтерфейсів.
Важливе непорозуміння: implements не змінює тип класу і не впливає на виведення типів його членів. Він лише перевіряє:
interface Checkable {
check(name: string): boolean;
}
class NameChecker implements Checkable {
check(s) { // s - неявно any, а не string з інтерфейсу!
return s.length > 0;
}
}
Параметр не отримує тип з інтерфейсу - його треба оголосити явно.
Структурна типізація: клас, який має потрібні методи, і так сумісний з інтерфейсом - навіть без implements. Ключове слово корисне як явний контракт: помилка з'являється в місці оголошення класу, а не там, де його десь передають.
Що обирати:
extends- коли є спільна реалізація, а нащадок справді «є різновидом» батька;implements- коли потрібен спільний контракт без спільного коду;- для повторного використання поведінки часто краща композиція (передати об'єкт-помічник), ніж глибокі ієрархії успадкування.
Інтерфейс може «розширювати» клас (interface X extends SomeClass) - береться лише його форма, разом із приватними членами, що на практиці трапляється рідко.