TypeScript: просунуті типи
20 питань · ~20 хв · Версія v3.0
Увійдіть, щоб продовжити
Дженерики й обмеження, звуження й union-типи, утилітні, умовні й зіставлені типи, виведення типів і шаблонні літерали - питання від middle до senior.
- За спробу
- 20
- У пулі
- 55
- Проходжень
- 0
- Середній бал
- -
- Пройшли на 70%+
- -
Питання для підготовки
39 питаньДля опису форми об'єкта обидва працюють майже однаково:
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 був помітним.
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 має бути простою й надійною, а для даних ззовні краще схема валідації.
Utility types - вбудовані перетворення типів, щоб не описувати схожі форми вручну:
interface User {
id: number;
name: string;
email: string;
password: string;
}
type UserUpdate = Partial<Omit<User, 'id'>>; // усі поля, крім id, необов'язкові
type PublicUser = Omit<User, 'password'>;
type UserPreview = Pick<User, 'id' | 'name'>;
type RolesMap = Record<'admin' | 'editor', string[]>;
type Frozen = Readonly<User>;
Ще корисні: Required, NonNullable, ReturnType<typeof fn>, Parameters<typeof fn>, Awaited<Promise<T>>.
Під капотом це mapped types - тип, що проходить по ключах іншого типу:
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Модифікатори можна і додавати, і знімати
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Перейменування ключів через as (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<Pick<User, 'name'>>; // { getName: () => string }
Навіщо на практиці: один джерельний тип (User), а похідні - форма оновлення, публічне представлення, стан форми - виводяться з нього. Додали поле в User - усі похідні оновилися, і компілятор покаже місця, які треба доробити.
Обережно: надто «розумні» типи важко читати й налагоджувати, а повідомлення про помилки в них стають незрозумілими. Якщо тип потрібно пояснювати, інколи простіше описати його явно.
Прочитати - ще не значить знати
20 питань, по одному на екран, ~20 хв. Після завершення - розбір кожної помилки з посиланням на питання.