Skip to main content

Утилітарний тип nonnullable в TypeScript

NonNullable<T> прибирає null та undefined з типу T, залишаючи тільки значення, які там реально можуть бути.

Теорія

TL;DR

  • Уяви кавовий фільтр: заливаєш тип з null і undefined всередині, а виходить тільки те, що потрібно.
  • Реалізація: T extends null | undefined ? never : T - TypeScript розкладає це по кожному члену union-у.
  • Використовуй після перевірки на null (if (!user) return), а не замість неї.
  • Прибирає тільки null/undefined на верхньому рівні. Вкладені null всередині об'єктів залишаються.
  • Семантично ідентичний до Exclude<T, null | undefined>, але назва краще передає намір.

Короткий приклад

typescript
type MaybeUser = { name: string } | null | undefined; type User = NonNullable<MaybeUser>; // { name: string } // На практиці: після перевірки на null function greet(user: User) { console.log(user.name); // optional chaining не потрібен }

Тип каже TypeScript: "до моменту, коли значення потрапляє в цю функцію, null вже оброблений зовні."

Головна відмінність від Exclude

NonNullable<T> і Exclude<T, null | undefined> дають однаковий результат для звичайних union-ів. Різниця в намірі. NonNullable у code review читається як "я гарантую, що тут немає null", а Exclude виглядає як загальне віднімання. Обирай NonNullable, коли конкретна мета - прибрати null і undefined.

Коли використовувати

  • Після runtime-перевірки: зробив if (!data) return;, далі функція приймає NonNullable<typeof data>.
  • Необов'язкові поля інтерфейсу: NonNullable<User["email"]> дає string замість string | undefined.
  • Фільтрація масивів з правильним звуженням типів (приклад нижче).
  • Після валідації API-відповіді, коли вже впевнений що значення є.

Не загортай початкові визначення типів у NonNullable. Якщо проп може бути null, так і пиши через | null. NonNullable - для звуженої, post-check сторони.

Як це працює всередині

TypeScript обчислює NonNullable<string | null | undefined>, розподіляючи умовний тип (conditional type) по кожному члену union-у. string extends null | undefined - хибно, тому залишається. null extends null | undefined - істинно, тому стає never. А never автоматично випадає з union-ів. Все це стирається до того, як TypeScript генерує JavaScript.

Типові помилки

Очікування глибокого очищення від null:

typescript
type HasInnerNull = { prop: string | null }; type Result = NonNullable<HasInnerNull | null>; // Result: { prop: string | null } <- вкладений null залишився!

Прибирається тільки верхньорівневий null. Для глибокого очищення пиши рекурсивний DeepNonNullable або використовуй Zod для runtime-валідації.

Заміна runtime-перевірки:

typescript
function bad(user: User | null) { const safe: NonNullable<typeof user> = user!; return safe.name; // компілюється, але падає при null у runtime }

NonNullable існує тільки на рівні типів. Завжди додавай справжній guard перед тим, як використовувати звужений тип.

Застосування там, де нічого прибирати:

typescript
type Pointless = NonNullable<string>; // все одно string

Помилки немає, але це зайвий шум. Використовуй тільки коли тип справді містить null або undefined.

Де зустрічається в реальному коді

  • React Query: після if (!data) return null; передаєш NonNullable<typeof data> у дочірні компоненти.
  • Express: як тільки req.user заповнений auth-middleware, в обробниках маршрутів використовуй NonNullable<Request["user"]>.
  • Next.js API routes: req.body з типом ParsedBody | null стає NonNullable<typeof req.body> після перевірки.
  • Необов'язкові поля інтерфейсу: отримуєш чистий string з string | null | undefined однією утилітою.

Питання на співбесіді

Q: Напиши реалізацію NonNullable<T>.
A: type NonNullable<T> = T extends null | undefined ? never : T;. У стандартній бібліотеці TypeScript є також варіант через T & {}, результат однаковий.

Q: Яка різниця між NonNullable<T> і Exclude<T, null | undefined>?
A: Для union-типів результат ідентичний. NonNullable краще підходить, коли мета - конкретно прибрати nullability, бо назва одразу говорить про намір.

Q: Чи прибирає NonNullable null із вкладених полів об'єкта?
A: Ні. Тільки верхньорівневі null і undefined з union-у. NonNullable<{ val: string | null } | null> дасть { val: string | null }. Для глибокого очищення потрібен рекурсивний mapped type.

Q: Як NonNullable взаємодіє з branded типами?
A: Бренд зберігається. NonNullable<(string & { readonly _brand: "UserId" }) | null> поверне брендований тип без змін, бо бренд не розширює null | undefined.

Приклади

Вилучення non-null із необов'язкових полів

typescript
interface User { id: string; name: string; email?: string; // string | undefined phone?: string | null; // string | null | undefined } type RequiredEmail = NonNullable<User["email"]>; // string type RequiredPhone = NonNullable<User["phone"]>; // string function sendConfirmation(email: RequiredEmail) { console.log(`Sending to ${email}`); // null-перевірка тут не потрібна }

User["email"] розкривається до string | undefined. Після NonNullable отримуємо просто string. Стандартний патерн: передаєш необов'язкове поле у функцію, яка чекає на реальне значення.

Фільтрація масиву з правильним звуженням типу

typescript
const users: (User | null | undefined)[] = [ { id: "1", name: "Alice", email: "alice@example.com" }, null, { id: "2", name: "Bob", email: "bob@example.com" }, undefined, ]; // filter(Boolean) прибирає null/undefined під час виконання, // але TypeScript все одно виводить (User | null | undefined)[] // Type predicate виправляє це: const validUsers = users.filter( (u): u is NonNullable<typeof u> => u != null ); // validUsers: User[] validUsers.forEach(u => console.log(u.name)); // Alice, Bob

Предикат u is NonNullable<typeof u> - це те, що звужує тип. Без нього TypeScript зберігає nullable union навіть після фільтрації. Цей патерн корисний, коли очищаєш масив з API-відповіді, де можуть зустрічатись null-значення.

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

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

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

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