Skip to main content

Обов'язковий тип утиліти в TypeScript

Required - утилітний тип TypeScript, який знімає ? з кожної необов'язкової властивості типу T, роблячи їх усі обов'язковими на рівні компіляції.

Теорія

TL;DR

  • Уявіть анкету, де всі поля були необов'язковими - і раптом на кожному з'явився штамп "заповнити обов'язково"
  • Головна різниця: знімає ? з кожної властивості, але тип не змінює (string | undefined стає просто string)
  • Використовуй після валідації, коли знаєш що всі поля є; Partial<T> залишай на етапі збору даних
  • Працює лише під час компіляції - у рантаймі жодного ефекту
  • Протилежність Partial<T>

Швидкий приклад

ts
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. Очікування перевірки в рантаймі

ts
// Required<T> НЕ перевіряє значення в рантаймі const data = JSON.parse(response) as Required<User>; // обходить усі перевірки

TypeScript довіряє касту. Якщо потрібна перевірка в рантаймі, поєднуй Required<T> з type guard або схемним валідатором на кшталт Zod.

2. Очікування що він обробляє вкладені необов'язкові поля

Ця поверхневість ловить майже всіх з першого разу. Застосовуєш Required<T> очікуючи повного покриття, а потім годину відлагоджуєш undefined у конфіг-об'єкті на третьому рівні вкладеності.

ts
type Nested = Required<{ a?: { b?: string } }>; // Результат: { a: { b?: string } } // a стало обов'язковим, але b всередині залишається необов'язковим

Для вкладених структур пиши рекурсивний тип:

ts
type DeepRequired<T> = T extends object ? { [K in keyof T]-?: DeepRequired<T[K]> } : T;

3. Застосування до union-типів

ts
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] } під капотом.

Приклади

Базовий: робимо необов'язкові поля обов'язковими

ts
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

ts
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 після збору та перевірки даних форми.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?