Утилітарний тип 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>, але назва краще передає намір.
Короткий приклад
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:
type HasInnerNull = { prop: string | null };
type Result = NonNullable<HasInnerNull | null>;
// Result: { prop: string | null } <- вкладений null залишився!Прибирається тільки верхньорівневий null. Для глибокого очищення пиши рекурсивний DeepNonNullable або використовуй Zod для runtime-валідації.
Заміна runtime-перевірки:
function bad(user: User | null) {
const safe: NonNullable<typeof user> = user!;
return safe.name; // компілюється, але падає при null у runtime
}NonNullable існує тільки на рівні типів. Завжди додавай справжній guard перед тим, як використовувати звужений тип.
Застосування там, де нічого прибирати:
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 із необов'язкових полів
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. Стандартний патерн: передаєш необов'язкове поле у функцію, яка чекає на реальне значення.
Фільтрація масиву з правильним звуженням типу
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-значення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.