Пошук уроків, статей та іншого контенту
Застосуйте Exclude, Extract і NonNullable для фільтрації об’єднань та вилучення null і undefined.
Об’єднання типів описує значення, яке може мати один із кількох типів:
type Status = "idle" | "loading" | "success" | "error";Іноді потрібно:
вилучити з об’єднання певні типи;
залишити лише типи, що відповідають заданій умові;
прибрати null та undefined.
TypeScript має для цього вбудовані узагальнені типи:
Exclude<T, U>;
Extract<T, U>;
NonNullable<T>.
Вони працюють на рівні типів і не змінюють значення під час виконання програми.
Exclude<T, U>Exclude<T, U> вилучає з T усі типи, які можна присвоїти типу U.
type Status = "idle" | "loading" | "success" | "error";
type FinishedStatus = Exclude<Status, "idle" | "loading">;
// "success" | "error"У цьому прикладі:
TypeScript розглядає кожен елемент об’єднання Status окремо.
Перевіряє, чи можна його присвоїти "idle" | "loading".
Вилучає відповідні елементи.
Схематично це можна записати так:
type Result = Exclude<T, U>;
// залишити елементи T, які не сумісні з UExclude також може працювати з об’єднаннями об’єктів:
type Result =
| { status: "success"; data: string }
| { status: "error"; message: string }
| { status: "loading" };
type NonLoadingResult = Exclude<Result, { status: "loading" }>;
// { status: "success"; data: string }
// | { status: "error"; message: string }Об’єкт із status: "loading" вилучено, оскільки він сумісний із другим параметром Exclude.
Exclude зручно використовувати, коли потрібно описати дозволені значення після вилучення кількох варіантів:
type AllRoles = "admin" | "editor" | "author" | "viewer";
type PublicRoles = Exclude<AllRoles, "admin">;
// "editor" | "author" | "viewer"Extract<T, U>Extract<T, U> працює протилежно до Exclude: він залишає з T лише ті типи, які можна присвоїти U.
type Status = "idle" | "loading" | "success" | "error";
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"Схематично:
type Result = Extract<T, U>;
// залишити елементи T, які сумісні з UExclude і Extracttype Permission = "read" | "write" | "delete";
type ReadOrWrite = Extract<Permission, "read" | "write">;
// "read" | "write"
type WithoutReadOrWrite = Exclude<Permission, "read" | "write">;
// "delete"Extract залишає збіги.
Exclude вилучає збіги.
Extract особливо корисний для вилучення конкретного варіанта з дискримінованого об’єднання:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "square"; size: number };
type Circle = Extract<Shape, { kind: "circle" }>;
// { kind: "circle"; radius: number }
type Rectangle = Extract<Shape, { kind: "rectangle" }>;
// { kind: "rectangle"; width: number; height: number }Тепер можна написати функцію, яка приймає лише конкретний варіант:
function getCircleArea(circle: Circle): number {
return Math.PI * circle.radius ** 2;
}
const circle: Circle = {
kind: "circle",
radius: 10,
};
console.log(getCircleArea(circle));Важливо, що Extract перевіряє сумісність типів. Об’єктний тип у другому параметрі має відповідати властивостям потрібного варіанта.
Exclude та Extract побудовані на умовних типах. Їхня спрощена форма виглядає так:
type MyExclude<T, U> = T extends U ? never : T;
type MyExtract<T, U> = T extends U ? T : never;Умовний тип має форму:
T extends U ? TypeIfTrue : TypeIfFalseДля об’єднання TypeScript застосовує цю перевірку до кожного його елемента.
Наприклад:
type MyExclude<T, U> = T extends U ? never : T;
type Result = MyExclude<"a" | "b" | "c", "b">;
// "a" | never | "c"
// "a" | "c"never не додає нового варіанта до об’єднання, тому "b" фактично зникає.
Аналогічно працює Extract:
type MyExtract<T, U> = T extends U ? T : never;
type Result = MyExtract<"a" | "b" | "c", "b" | "c">;
// never | "b" | "c"
// "b" | "c"Цю поведінку називають розподільністю умовного типу щодо об’єднання.
NonNullable<T>NonNullable<T> вилучає з типу T значення null та undefined.
type Value = string | number | null | undefined;
type DefinedValue = NonNullable<Value>;
// string | numberПо суті, NonNullable<T> еквівалентний такому типу:
type MyNonNullable<T> = Exclude<T, null | undefined>;type UserName = string | null | undefined;
function printUserName(name: NonNullable<UserName>): void {
console.log(name.toUpperCase());
}
printUserName("Olena");
// printUserName(null); // Помилка типів
// printUserName(undefined); // Помилка типівПараметр функції має тип string, оскільки null та undefined були вилучені.
NonNullable для властивостейtype User = {
id: number;
email: string | null;
phone?: string;
};
type RequiredEmail = NonNullable<User["email"]>;
// string
type DefinedPhone = NonNullable<User["phone"]>;
// stringВираз User["phone"] має тип string | undefined, оскільки властивість необов’язкова. Після застосування NonNullable залишається лише string.
type ApiState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
type CompletedState = Exclude<
ApiState,
{ status: "idle" } | { status: "loading" }
>;
// { status: "success"; data: string[] }
// | { status: "error"; message: string }
type SuccessState = Extract<ApiState, { status: "success" }>;
// { status: "success"; data: string[] }
type ErrorState = Extract<ApiState, { status: "error" }>;
// { status: "error"; message: string }
type OptionalData = string[] | null | undefined;
type Data = NonNullable<OptionalData>;
// string[]
function printSuccess(state: SuccessState): void {
console.log(`Отримано елементів: ${state.data.length}`);
}
function printError(state: ErrorState): void {
console.error(state.message);
}
const success: SuccessState = {
status: "success",
data: ["TypeScript", "JavaScript"],
};
const error: ErrorState = {
status: "error",
message: "Не вдалося завантажити дані",
};
printSuccess(success);
printError(error);У цьому прикладі:
Exclude прибирає стани "idle" та "loading";
Extract вибирає конкретні стани "success" і "error";
NonNullable прибирає null та undefined з типу даних.
Утилітні типи можна комбінувати:
type ApiResponse =
| { status: "success"; data: string[] }
| { status: "success"; data: null }
| { status: "error"; message: string }
| null
| undefined;
type SuccessResponse = Extract<ApiResponse, { status: "success" }>;
// { status: "success"; data: string[] }
// | { status: "success"; data: null }
type ResponseData = SuccessResponse["data"];
// string[] | null
type DefinedResponseData = NonNullable<ResponseData>;
// string[]Послідовність операцій тут така:
Extract залишає варіанти зі статусом "success".
Доступ до ["data"] отримує тип їхньої властивості.
NonNullable вилучає null.
strictNullChecks і NonNullableКоректна робота з null та undefined залежить від опції strictNullChecks.
У строгому режимі null і undefined є окремими типами:
let value: string | null = null;
// value.toUpperCase(); // Помилка: value може бути nullПісля перевірки TypeScript звужує тип:
function printValue(value: string | null | undefined): void {
if (value !== null && value !== undefined) {
console.log(value.toUpperCase());
}
}NonNullable корисний для опису результату такої фільтрації на рівні типів:
type Input = string | null | undefined;
type SafeInput = NonNullable<Input>;
// stringСам по собі NonNullable не перевіряє значення під час виконання. Він лише змінює статичний тип. Перевірка фактичного значення має бути реалізована в коді.
Exclude вилучить тип за назвою властивостіtype Event =
| { type: "click"; x: number }
| { type: "keydown"; key: string };
// Це не вилучає подію "click":
type Wrong = Exclude<Event, { type: string }>;{ type: "click"; x: number } сумісний із { type: string }, тому в цьому конкретному прикладі обидва варіанти можуть бути вилучені.
Для точного вилучення потрібно вказати конкретний дискримінатор:
type WithoutClick = Exclude<Event, { type: "click" }>;
// { type: "keydown"; key: string }Exclude і Extracttype Role = "admin" | "editor" | "viewer";
type OnlyAdmin = Extract<Role, "admin">;
// "admin"
type WithoutAdmin = Exclude<Role, "admin">;
// "editor" | "viewer"Якщо потрібно залишити збіги — використовуйте Extract. Якщо потрібно їх прибрати — Exclude.
NonNullable до значення під час виконанняconst value: string | null = Math.random() > 0.5 ? "ok" : null;
// NonNullable не є функцією і не змінює value під час виконання.NonNullable застосовують до типів:
type DefinedValue = NonNullable<typeof value>;
// stringАле це не означає, що змінна value автоматично перестала бути null. Для безпечної роботи все одно потрібна перевірка:
if (value !== null) {
console.log(value.toUpperCase());
}Extract<T, U> не шукає властивість за її назвою. Він залишає лише ті варіанти, які структурно сумісні з U.
type Item =
| { kind: "text"; value: string }
| { kind: "number"; value: number };
type TextItem = Extract<Item, { kind: "text" }>;
// { kind: "text"; value: string }Якщо в умові вказати несумісну структуру, результатом буде never:
type BooleanItem = Extract<Item, { kind: "boolean" }>;
// neverExclude<T, U> вилучає з T типи, сумісні з U.
Extract<T, U> залишає з T лише типи, сумісні з U.
NonNullable<T> вилучає з T null та undefined.
Усі три утиліти працюють зі статичними типами, а не зі значеннями під час виконання.
Extract зручно застосовувати до дискримінованих об’єднань об’єктів.
NonNullable<T> еквівалентний Exclude<T, null | undefined>.
Для надійної роботи з null і undefined варто використовувати strictNullChecks.