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

Junior: питання на співбесіді з теми «Патерни й типобезпека»

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

5 питань

satisfies перевіряє, що значення відповідає типу, але не замінює виведений тип значення на цей тип.

Проблема з анотацією:

type Route = { path: string; auth?: boolean };

const routes: Record<string, Route> = {
  home: { path: '/' },
  admin: { path: '/admin', auth: true },
};

routes.admin.path;   // ok
routes.nope.path;    // теж компілюється - тип каже «будь-який рядок-ключ»

Анотація Record<string, Route> «стерла» знання про конкретні ключі: TypeScript більше не знає, що є лише home і admin.

З satisfies:

const routes = {
  home: { path: '/' },
  admin: { path: '/admin', auth: true },
} satisfies Record<string, Route>;

routes.admin.auth;   // ok
routes.nope;         // помилка: такого ключа немає

Об'єкт перевірено на відповідність Route (друкарська помилка в pth чи auth: 'yes' дасть помилку), а тип лишився точним - з конкретними ключами й значеннями.

Де це корисно:

  • конфігурації й мапи: маршрути, переклади, налаштування таблиць, словники статусів;
  • разом з as const - точні літеральні типи плюс перевірка форми:
const statusColors = {
  paid: 'green',
  pending: 'amber',
} as const satisfies Record<OrderStatus, string>;

Якщо в OrderStatus з'явиться новий статус, TypeScript вкаже, що мапа неповна.

Порівняння трьох способів:

Запис Перевірка Тип значення
const x: T = ... так T (широкий)
const x = ... as T майже ні T
const x = ... satisfies T так точний виведений

as тут - найгірший варіант: приведення типу пропускає помилки, які satisfies і анотація зловили б.

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

as const каже TypeScript вивести найвужчий тип і зробити все лише для читання:

const roles = ['admin', 'editor', 'viewer'];            // string[]
const roles2 = ['admin', 'editor', 'viewer'] as const;  // readonly ['admin', 'editor', 'viewer']

type Role = (typeof roles2)[number];   // 'admin' | 'editor' | 'viewer'

roles2.push('guest');   // помилка: масив лише для читання

Головний прийом - одне джерело правди для значень і типу. Список значень потрібен і під час виконання (випадний список, валідація), і як тип. З as const тип виводиться зі значень, і вони не розходяться:

const ORDER_STATUSES = ['new', 'paid', 'shipped'] as const;
type OrderStatus = (typeof ORDER_STATUSES)[number];

function isOrderStatus(value: string): value is OrderStatus {
  return (ORDER_STATUSES as readonly string[]).includes(value);
}

Це часто краща альтернатива enum: звичайний JavaScript-масив без особливого синтаксису.

Об'єкти:

const config = { api: { timeout: 5000 } } as const;
config.api.timeout = 10;   // помилка - readonly на всіх рівнях

readonly у типах - для параметрів і полів, які функція не повинна змінювати:

function total(items: readonly CartItem[]): number {
  items.sort();   // помилка: sort змінює масив
  return items.reduce((sum, i) => sum + i.price, 0);
}

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

ReadonlyArray<T> / readonly T[] прибирають з типу методи, що змінюють масив (push, sort, splice).

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

  • це лише перевірка компілятора. Під час виконання об'єкт звичайний - змінити його можна через as any чи з JavaScript-коду. Для справжньої незмінності - Object.freeze;
  • readonly поверхневий у звичайних типах: readonly items: Item[] забороняє замінити масив, але не змінити його вміст. Для вкладених структур - readonly на кожному рівні чи утиліта на кшталт DeepReadonly;
  • масив readonly string[] не можна передати туди, де очікують string[] - функції, що не змінюють масив, варто оголошувати з readonly в параметрі.

Докладніше в документації: Const-твердження

Постфіксний ! каже компілятору: «це значення точно не null і не undefined». Компілятор вірить на слово й прибирає null | undefined з типу.

const input = document.querySelector('#email')!;   // HTMLElement замість HTMLElement | null
const price = prices.get('coffee')!;               // number замість number | undefined

! нічого не перевіряє під час виконання. Якщо значення все ж null, помилка виникне пізніше й далеко від причини: Cannot read properties of null. TypeScript саме для того й попереджав.

Коли ! доречний:

  • значення гарантовано існує з причин, яких компілятор не бачить, і ця гарантія очевидна поруч у коді;
  • ініціалізація, яку TypeScript не відстежує (поле класу, яке заповнює фреймворк), - хоча для полів краще field!: Type у оголошенні з коментарем, ніж ! при кожному використанні.

Краще альтернативи в більшості випадків:

  • явна перевірка з помилкою - падіння одразу з зрозумілим текстом:
const input = document.querySelector<HTMLInputElement>('#email');
if (!input) throw new Error('Поле #email не знайдено');
input.value;   // тут уже HTMLInputElement
  • функція-помічник:
function assertDefined<T>(value: T, message: string): asserts value is NonNullable<T> {
  if (value == null) throw new Error(message);
}
  • опціональний ланцюжок і значення за замовчуванням, якщо відсутність нормальна: prices.get('coffee') ?? 0;
  • переписати код так, щоб тип був точним: зберігати знайдений елемент у змінній після перевірки, а не шукати двічі.

Типові місця, де ! приховує баги:

  • map.get(key)! - ключа може не бути;
  • array.find(...)! - елемента може не знайтися;
  • useRef<HTMLDivElement>(null).current! в ефекті, що може виконатися до монтування;
  • process.env.API_KEY! - змінну оточення можуть не задати.

Лінтер (@typescript-eslint/no-non-null-assertion) змушує обґрунтовувати кожен ! - корисне правило для команди.

Важливо: strict у TypeScript 6+ увімкнено за замовчуванням, тож перевірки на null діють навіть без явного "strict": true - і ! стає помітнішою «дірою» в цих перевірках.

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

value as T - твердження типу: розробник каже компілятору «вважай це значення типом T». Нічого не перетворюється й не перевіряється під час виконання.

const user = (await response.json()) as User;
user.email.toLowerCase();   // впаде, якщо API повернув { error: '...' }

Компілятор довіряє, а реальні дані можуть бути іншими. Помилка переноситься з місця, де дані прийшли, у випадкове місце далі в коді.

Що as дозволяє, а що ні:

  • звужувати й розширювати в межах сумісних типів (unknown → User, HTMLElement → HTMLInputElement);
  • приведення між несумісними типами ('текст' as number) - помилка компіляції. Але подвійне as unknown as T обходить і це - верна ознака, що щось не так.

Альтернативи:

1. Перевірка під час виконання для зовнішніх даних (API, localStorage, форми):

import { z } from 'zod';

const UserSchema = z.object({ id: z.number(), email: z.email() });
type User = z.infer<typeof UserSchema>;

const user = UserSchema.parse(await response.json());   // кидає помилку, якщо дані не ті

2. Користувацький type guard:

function isUser(value: unknown): value is User {
  return typeof value === 'object' && value !== null && 'email' in value;
}

3. Звуження вбудованими перевірками: instanceof, typeof, in, перевірка поля-дискримінатора.

4. Типізоване API замість приведення: document.querySelector<HTMLInputElement>('input[name=email]') замість as HTMLInputElement; генерік у useState<User | null>(null).

5. satisfies для об'єктів-літералів - перевірка форми без втрати точного типу.

Коли as допустимий:

  • as const - не приведення, а звуження до літеральних типів;
  • тести й моки ({} as Partial<Service> as Service) - з розумінням ризику;
  • місця, де ви знаєте більше за компілятор і це очевидно з контексту (наприклад, після власної перевірки, яку TypeScript не розпізнав).

Правило лінтера @typescript-eslint/consistent-type-assertions дає змогу заборонити as для об'єктних літералів і змусити використовувати анотації чи satisfies.

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

TypeScript перевіряє типи структурно: об'єкт підходить до типу, якщо має всі потрібні властивості потрібних типів. Зайві властивості цьому не заважають.

type Point = { x: number; y: number };

const point3d = { x: 1, y: 2, z: 3 };
const p: Point = point3d;   // ок - є x і y, z просто ігнорується

Але для «свіжого» об'єкта-літерала діє додаткова перевірка зайвих властивостей:

const p: Point = { x: 1, y: 2, z: 3 };
// помилка: Object literal may only specify known properties, and 'z' does not exist in type 'Point'

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

createUser({ name: 'Оля', emial: 'olia@example.com' });   // emial замість email - зловлено

Але якщо об'єкт уже існує й має ширшу форму, передати його туди, де потрібна частина полів, - нормальна практика структурної типізації.

Де перевірка зайвих властивостей не спрацьовує:

  • об'єкт спершу присвоєно змінній, а потім передано;
  • результат функції, розгортання ({ ...defaults, extra: 1 } перевіряється, а от { ...obj }, де obj має зайве, - ні);
  • тип має індексну сигнатуру ([key: string]: unknown) - тоді будь-які ключі допустимі;
  • приведення через as.

Пастка з опціональними полями:

type Options = { timeout?: number; retries?: number };

const userOptions = { timeout: 500, retires: 3 };   // друкарська помилка в retries
const options: Options = userOptions;               // жодної помилки! усі поля опціональні

Тип, у якого всі поля необов'язкові («слабкий тип»), TypeScript частково захищає: якщо в об'єкта немає жодного спільного поля з типом ({ timout: 500 }), буде помилка. Але одна правильна властивість поруч з друкарською помилкою - і перевірка мовчить, а retries тихо лишається не заданим.

Що робити: передавати літерали напряму в місця з типом, використовувати satisfies для об'єктів-конфігурацій, а для зовнішніх даних - валідацію під час виконання (Zod), яка може відкидати невідомі ключі (.strict()).

Докладніше в документації: Перевірка зайвих властивостей