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

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.

Докладніше в документації: Оператор 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: параметри типу в обмеженнях