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

TypeScript: Загальний

20 питань · ~18 хв · Версія v3.0

Увійдіть, щоб продовжити

Система типів: union/intersection, generics, keyof, utility types, звуження, structural typing.

За спробу
20
У пулі
100
Проходжень
0
Середній бал
-
Пройшли на 70%+
-

Питання для підготовки

100 питань

Для опису форми об'єкта обидва працюють майже однаково:

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

type UserType = {
  id: number;
  name: string;
};

Відмінності:

  • type описує будь-що, не лише об'єкти: об'єднання (type Status = 'draft' | 'published'), кортежі, примітиви, умовні й зіставлені типи. interface - лише форму об'єкта (і функції, класу).
  • Злиття оголошень: два interface з однаковим іменем зливаються в один. Так розширюють чужі типи - наприклад, додають поле до Window. type з тим самим іменем - помилка.
  • Розширення: interface Admin extends User {} проти type Admin = User & { role: string }. При конфлікті полів extends одразу дає зрозумілу помилку, а перетин & може мовчки дати never.
  • Повідомлення про помилки з інтерфейсами інколи читабельніші, а великі ієрархії інтерфейсів компілятор перевіряє трохи швидше.

Практичне правило: interface - для форм об'єктів і публічних API (їх можна розширити), type - для об'єднань, кортежів і всього обчислюваного. Головне - послідовність у кодовій базі: обидва варіанти правильні.

Докладніше в документації: Аліаси типів та інтерфейси

Обидва означають «тут може бути будь-яке значення», але з протилежним ставленням до перевірок:

  • any вимикає перевірку типів. З ним можна робити що завгодно - викликати методи, звертатися до полів, передавати куди завгодно. Компілятор мовчить, а помилка вилізе під час виконання.
  • unknown - безпечний аналог: присвоїти в нього можна будь-що, але використати значення не можна, доки не доведете, що воно потрібного типу.
const a: any = JSON.parse(text);
a.user.name.toUpperCase();     // компілюється - і може впасти

const u: unknown = JSON.parse(text);
u.user;                        // помилка компіляції

if (typeof u === 'object' && u !== null && 'user' in u) {
  // тут TypeScript знає більше
}

Чому any шкідливий: він «заразний». Значення, отримане з any, теж any, і відсутність перевірок непомітно розповзається кодом.

Коли що:

  • unknown - для даних ззовні: JSON.parse, відповідь API, catch (error) (у строгому режимі error і так unknown), значення з localStorage. Звужуєте перевірками або схемою валідації (Zod).
  • any - як тимчасовий захід при міграції з JavaScript, з коментарем і планом прибрати.

У tsconfig варто тримати "strict": true (він вмикає noImplicitAny), а лінтер налаштувати так, щоб явний any був помітним.

Докладніше в документації: unknown

Файл декларацій .d.ts описує лише типи - без реалізації. Він каже TypeScript, які функції, класи й змінні існують і якого вони типу, а сам код живе деінде (у JavaScript-файлі).

// slugify.d.ts
export default function slugify(text: string, options?: { lower?: boolean }): string;

Звідки беруться декларації:

1. Пакет сам постачає типи. Бібліотека, написана на TypeScript, генерує .d.ts при збиранні (declaration: true) і вказує їх у package.json. Більшість сучасних пакетів так і роблять - нічого встановлювати не треба.

2. Пакети @types/* - для бібліотек, написаних на JavaScript без власних типів. Їх пишуть і підтримують спільнотою в репозиторії DefinitelyTyped:

npm install -D @types/lodash

Версія @types/lodash повторює мажорну й мінорну версію самої бібліотеки - їх варто тримати узгодженими.

3. Вбудовані декларації TypeScript - lib.dom.d.ts, lib.es2025.d.ts тощо. Набір обирається опціями target і lib.

4. Власні декларації в проєкті - для бібліотек без типів, глобальних змінних, файлів-ресурсів (*.svg, *.css).

Як TypeScript знаходить типи імпорту: спершу в самому пакеті (types/exports у package.json), потім у node_modules/@types/назва-пакета.

Важлива зміна в TypeScript 6/7: опція types за замовчуванням тепер порожня ([]). Раніше TypeScript автоматично підключав усі пакети з node_modules/@types як глобальні - через це в кожному проєкті були типи process, describe тощо. Тепер глобальні типи треба перелічити явно:

{ "compilerOptions": { "types": ["node", "vitest/globals"] } }

На типи пакетів, які імпортуються (import _ from 'lodash'), це не впливає - лише на глобальні.

skipLibCheck: true - не перевіряти .d.ts залежностей: значно прискорює збирання, а помилки в чужих деклараціях вам однаково не виправити.

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

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. Параметр типу має сенс, коли він пов'язує кілька місць: аргументи між собою або аргумент з результатом.

Докладніше в документації: Generics

Звуження (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 має бути простою й надійною, а для даних ззовні краще схема валідації.

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

Прочитати - ще не значить знати

20 питань, по одному на екран, ~18 хв. Після завершення - розбір кожної помилки з посиланням на питання.