Junior: питання на співбесіді з теми «Просунуті типи»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
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.
У позиції типу 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 - тип під час компіляції
Це різні оператори з однаковою назвою: перший працює в коді, другий - лише в позиції типу.
Індексований тип доступу - отримати тип властивості іншого типу тим самим синтаксисом, що й доступ до поля об'єкта:
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.
Літеральний тип - тип, що допускає одне конкретне значення:
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 під час виконання не перевіряються - потрібна валідація.
Параметр типу може бути обмежений іншим параметром - так описують зв'язок між аргументами функції.
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' немає серед ключів
Як це працює:
- TypeScript виводить
Tз першого аргументу - тип елементів масиву; K extends keyof Tдозволяє другим аргументом лише ключіT;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: параметри типу в обмеженнях