Обов'язковий тип утиліти в TypeScript
Required? з кожної необов'язкової властивості типу T, роблячи їх усі обов'язковими на рівні компіляції.
Теорія
TL;DR
- Уявіть анкету, де всі поля були необов'язковими - і раптом на кожному з'явився штамп "заповнити обов'язково"
- Головна різниця: знімає
?з кожної властивості, але тип не змінює (string | undefinedстає простоstring) - Використовуй після валідації, коли знаєш що всі поля є;
Partial<T>залишай на етапі збору даних - Працює лише під час компіляції - у рантаймі жодного ефекту
- Протилежність
Partial<T>
Швидкий приклад
interface User {
name: string;
age?: number; // необов'язково
}
type StrictUser = Required<User>;
const user: StrictUser = { name: "Alice", age: 30 }; // працює
// const bad: StrictUser = { name: "Bob" }; // Помилка: age відсутнійStrictUser тепер вимагає і name, і age. Пропусти будь-яке - TypeScript відразу повідомить про помилку.
Головна різниця
Required<T> тільки знімає модифікатор ? - і нічого більше. age?: number стає age: number, а не age: number | undefined. Це важливо, коли пишеш функції що очікують гарантоване значення: TypeScript ловить помилки під час компіляції, а не в рантаймі.
Коли використовувати
- Дані збирались як
Partial<T>, потім валідувались: використовуйRequired<T>як тип повернення функції валідації - Відповідь від API після успішного запиту: структура вже повна
- Props React-компонента з усіма значеннями за замовчуванням: передай
Required<Props>дочірньому компоненту - Генерація моків: уникнеш несподіваних
undefinedу тестах - Після type guard, що підтвердив наявність усіх полів:
data is Required<FormData>читається зрозуміло
Як це обробляє компілятор
TypeScript перебирає дескриптори кожної властивості T. Якщо властивість позначена як необов'язкова (прапорець ?), компілятор переписує її в обов'язкову. Під капотом це mapped type: { [K in keyof T]-?: T[K] }. Синтаксис -? знімає модифікатор необов'язковості. JavaScript при цьому не змінюється - суто перевірка на рівні компіляції.
Поширені помилки
1. Очікування перевірки в рантаймі
// Required<T> НЕ перевіряє значення в рантаймі
const data = JSON.parse(response) as Required<User>; // обходить усі перевіркиTypeScript довіряє касту. Якщо потрібна перевірка в рантаймі, поєднуй Required<T> з type guard або схемним валідатором на кшталт Zod.
2. Очікування що він обробляє вкладені необов'язкові поля
Ця поверхневість ловить майже всіх з першого разу. Застосовуєш Required<T> очікуючи повного покриття, а потім годину відлагоджуєш undefined у конфіг-об'єкті на третьому рівні вкладеності.
type Nested = Required<{ a?: { b?: string } }>;
// Результат: { a: { b?: string } }
// a стало обов'язковим, але b всередині залишається необов'язковимДля вкладених структур пиши рекурсивний тип:
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;3. Застосування до union-типів
type Bad = Required<string | { a?: number }>;
// Поводиться несподівано - уникай Required на union-типах напрямуЯкщо тип - union, застосовуй Required до кожного члена окремо або спочатку використовуй Extract.
4. Застосування до примітивів
Required<string> повертає string без змін. Знімати нічого.
Де зустрічається
- Обробник форми в React:
validateForm(data: FormData): data is Required<FormData>- type guard після перевірки всіх полів - Express middleware:
Request<{}, {}, Required<CreateUserBody>>після валідації тіла запиту - React Query:
Required<Omit<Response, 'data'>>на шляху успіху - Zod:
z.infer<typeof schema> & Required<PartialFields>для часткових схем - TanStack Table: об'єднання column def через
Required<Partial<ColumnDef>>
Follow-up питання
Q: Який тип у Required<{ a?: string }>?
A: { a: string }. Знак ? знімається, тип залишається string, а не string | undefined.
Q: Чи впливає Required<T> на вкладені об'єкти?
A: Ні. Він знімає ? тільки з властивостей верхнього рівня. Вкладені необов'язкові поля залишаються необов'язковими. Для вкладених структур використовуй кастомний DeepRequired<T>.
Q: У чому різниця між Required<T> і Omit<T, never>?
A: Структурний результат однаковий, але Required<T> - це семантичний вибір. Omit<T, never> є обхідним шляхом і ламається на не-об'єктних типах.
Q: Як зробити Required тільки для конкретних ключів?
A: type RequiredSpecific<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>. Поєднує Omit і Pick щоб точно вибрати лише потрібні ключі.
Q: Чому в mapped types є синтаксис -??
A: Це синтаксис модифікатора. +? додає необов'язковість, -? знімає її. Required<T> використовує { [K in keyof T]-?: T[K] } під капотом.
Приклади
Базовий: робимо необов'язкові поля обов'язковими
interface Product {
id: number;
name: string;
description?: string;
price?: number;
}
type FullProduct = Required<Product>;
const item: FullProduct = {
id: 1,
name: "Keyboard",
description: "Mechanical", // тепер обов'язково
price: 99, // тепер обов'язково
};
// const broken: FullProduct = { id: 1, name: "Mouse" }; // Помилка: поля відсутніДо Required<T> можна було створити Product без description і price. Після - TypeScript не дозволить на рівні типів.
Середній рівень: обробник валідації форми з type guard
interface FormData {
email: string;
phone?: string;
address?: string;
}
function validateForm(data: FormData): data is Required<FormData> {
return !!data.phone && !!data.address;
}
function submitUser(formData: FormData) {
if (validateForm(formData)) {
// TypeScript знає що всі поля є всередині цього блоку
console.log(`${formData.email} - ${formData.phone} - ${formData.address}`);
}
}
const data: FormData = {
email: "user@example.com",
phone: "555-1234",
address: "10 Main St",
};
submitUser(data);Type guard data is Required<FormData> звужує тип всередині блоку if. Ніякого кастингу. Цей патерн часто зустрічається в API-роутах Next.js після збору та перевірки даних форми.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.