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