Middle: питання на співбесіді з теми «Типи TypeScript»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Generics - параметри типів: функція, клас чи тип працює з різними типами, зберігаючи зв'язок між ними. Без generics довелося б вибирати між дублюванням коду й any.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(['a', 'b']); // string | undefined
TypeScript сам вивів T з аргументу - писати first<number>(...) не потрібно.
Обмеження extends - вимога до параметра типу:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('abc', 'de'); // працює з рядками
longest([1, 2], [3]); // і з масивами
longest(10, 20); // помилка: у number немає length
keyof разом з generics - типобезпечний доступ до полів:
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
pluck(users, 'email'); // string[]
pluck(users, 'emial'); // помилка компіляції: опечатка видна одразу
Де generics щодня: Array<T>, Promise<T>, Map<K, V>, хуки (useState<User | null>(null)), типізовані API-клієнти (get<User>('/users/1')).
Пастка: generic, який використовується лише раз (function log<T>(x: T): void), нічого не дає - тут достатньо unknown. Параметр типу має сенс, коли він пов'язує кілька місць: аргументи між собою або аргумент з результатом.
Звуження (narrowing) - TypeScript аналізує перевірки в коді й усередині гілки вважає тип вужчим, ніж оголошений.
function format(value: string | number | null) {
if (value === null) return '-';
if (typeof value === 'number') {
return value.toFixed(2); // тут value: number
}
return value.trim(); // а тут value: string
}
Що звужує тип:
typeof x === 'string',x instanceof Date,Array.isArray(x);- перевірки на
null/undefinedі на істинність; 'field' in obj- чи є властивість;- порівняння з літералом (
status === 'paid') - основа discriminated unions; - ранній
returnчиthrow: після них тип уже звужено до кінця функції.
Власний type guard - функція з типом повернення value is T:
interface Cat { meow(): void }
interface Dog { bark(): void }
function isCat(animal: Cat | Dog): animal is Cat {
return 'meow' in animal;
}
if (isCat(pet)) pet.meow();
Assertion function - кидає виняток, якщо умова не виконана, і звужує тип після виклику:
function assertDefined<T>(value: T): asserts value is NonNullable<T> {
if (value == null) throw new Error('Значення відсутнє');
}
Пастка: type guard - це обіцянка, яку компілятор не перевіряє. Якщо isCat повертає true для собаки, TypeScript повірить. Тому логіка в guard має бути простою й надійною, а для даних ззовні краще схема валідації.
Кортеж - масив фіксованої структури: відомо, скільки елементів і якого типу кожен.
type Point = [number, number];
type Entry = [key: string, value: number]; // іменовані елементи
const p: Point = [10, 20];
const [x, y] = p;
Імена елементів (key, value) нічого не змінюють у поведінці - вони лише для читабельності й підказок редактора.
Де кортежі зустрічаються:
- повернення кількох значень:
useStateв React повертає[value, setValue]- кортеж, тому деструктуризація дає правильні типи кожному елементу; Object.entries, пари ключ-значення;- параметри функцій як кортеж:
Parameters<typeof fn>- кортеж типів аргументів.
Необов'язкові елементи - лише в кінці:
type Range = [start: number, end?: number];
const r1: Range = [1];
const r2: Range = [1, 5];
Залишкові (rest) елементи - змінна довжина:
type Command = [name: string, ...args: string[]];
type WithFlag = [...items: number[], flag: boolean]; // rest може бути й не в кінці
Варіадичні кортежі - комбінування типів кортежів у узагальненнях:
function concat<A extends unknown[], B extends unknown[]>(a: [...A], b: [...B]): [...A, ...B] {
return [...a, ...b];
}
const result = concat([1, 'a'], [true]); // [number, string, boolean]
Так типізуються функції-обгортки, що додають аргументи на початок чи в кінець (bind, middleware, каррування).
Пастки:
- виведення типу масиву - не кортеж:
const pair = [1, 'a']має тип(string | number)[]. Для кортежу - анотація абоas const(тодіreadonly [1, 'a']); - кортеж можна змінити через методи масиву:
point.push(30)компілюється для звичайного кортежу.readonly [number, number]це забороняє; - довгі кортежі погано читаються: якщо елементів більше 2-3, краще об'єкт з іменованими полями -
{ lat, lng }замість[number, number].
Обидва способи складають тип з кількох частин:
interface HasId { id: number }
interface HasTimestamps { createdAt: string; updatedAt: string }
// успадкування інтерфейсів
interface Post extends HasId, HasTimestamps {
title: string;
}
// перетин типів
type Comment = HasId & HasTimestamps & { body: string };
Для простих випадків результат однаковий. Різниця - у конфліктах і в тому, як компілятор з ними працює.
1. Конфлікт властивостей.
interface A { status: string }
interface B extends A { status: number } // помилка одразу: number не сумісний з string
type C = { status: string } & { status: number }; // помилки немає...
// ...але C['status'] - це never: жодне значення не підійде, і об'єкт типу C не створити
extends повідомляє про несумісність в оголошенні. Перетин мовчки дає never, і помилка з'являється далеко - там, де намагаються створити об'єкт.
2. Продуктивність перевірки типів. Інтерфейси кешуються як іменовані типи; компілятор порівнює їх за назвою. Великі перетини перераховуються щоразу. Документація TypeScript з продуктивності радить у великих кодових базах складати об'єктні типи через interface ... extends, а не через довгі ланцюжки &.
3. Що можна скласти. extends в інтерфейсі - лише з об'єктними типами (й класами). Перетин працює з будь-якими типами, включно з об'єднаннями й узагальненнями:
type WithMeta<T> = T & { meta: { requestId: string } };
type Branded = string & { readonly __brand: 'UserId' };
Перетин з об'єднаннями розподіляється: (A | B) & C - це (A & C) | (B & C).
Перетин примітивів - string & number - дає never: значення, що водночас рядок і число, не існує.
Практичні правила:
- об'єктні сутності й їх розширення -
interface ... extends: краща діагностика й швидкість; - узагальнені «домішки» (
T & { ... }), брендовані типи, композиція типів з утиліт - перетин; - якщо перетин дає несподіваний
never, - майже завжди конфлікт однойменних властивостей.
void - функція нічого корисного не повертає; результат не слід використовувати:
function log(message: string): void {
console.log(message);
}
Особливість: тип функції з void, наприклад () => void для колбеку, дозволяє передати функцію, що щось повертає. Результат просто ігнорується:
const ids: number[] = [];
[1, 2].forEach((n) => ids.push(n)); // push повертає number, але forEach очікує () => void - ок
Це навмисно: інакше багато звичайних колбеків довелося б обгортати.
undefined як тип повернення - функція явно повертає undefined, і результат можна використовувати як значення. З TS 5.1 функцію з типом undefined можна не закінчувати return.
never - функція ніколи не повертає керування:
function fail(message: string): never {
throw new Error(message);
}
function loop(): never {
while (true) {}
}
Після виклику такої функції код недосяжний, і TypeScript це враховує при звуженні:
const user = findUser(id) ?? fail('Користувача не знайдено'); // user: User, без undefined
never також - порожній тип: значення такого типу не існує. Тому його використовують для перевірки вичерпності (const _exhaustive: never = value) і він зникає з об'єднань: string | never - це string.
unknown - функція повертає щось, але тип невідомий: викликач має перевірити перед використанням. Правильний тип для JSON.parse, результатів сторонніх бібліотек без типів, catch (error).
Підсумок:
| Тип | Значення |
|---|---|
void |
«результат не використовувати» |
undefined |
«повертає саме undefined» |
never |
«не повертає взагалі» (виняток, нескінченний цикл, вихід з процесу) |
unknown |
«повертає щось, перевір перед використанням» |
Пастка з асинхронністю: async функція, що нічого не повертає, має тип Promise<void>, а не void. Передача її туди, де очікується () => void (обробник події), дозволена, але відхилений проміс ніхто не обробить - лінтер no-misused-promises це помічає.