Представим, что у нас есть некоторый метод API, который возвращает нам данные для отображения таблицы. В целях этого поста имеет значение лишь то, что результатом этого запроса может быть как минимум два состояния: успех и какие-то нужные нам данные или ошибка. В первом случае мы захотим отобразить страницу, во втором — показать какой-нибудь снекбар с неутешительным сообщением. Попробуем типизировать эти состояния
Типизируем «в лоб»
Самый простой способ — написать два интерфейса и сформировать тип результата вызова ручки через их объединение
interface RequestSuccess {
data: string[]
}
interface RequestError {
message: string
}
type RequestResult = RequestSuccess | RequestError;
Должно сработать, но вот проблема, тайпскрипт не понимает, что именно будет внутри переменной, в которой мы сохраним результат вызова апи: данные или ошибка? И он не даст нам спокойно обратиться ни к полю data, ни к полю message.
function request(): RequestResult { ... }
const result = request();
// Property 'message' does not exist on type 'RequestResult'
// Property 'message' does not exist on type 'RequestSuccess'
result.message;
// Property 'data' does not exist on type 'RequestResult'
// Property 'data' does not exist on type 'RequestError'
result.data; //
Довольно бесполезная получилась типизация. Давайте исправлять
Корректная типизация
Благо, что исправлять много не нужно. Для начала, правда, придется добиться того, чтобы в ответе появилось еще одно поле, общее для всех статусов запроса, но с разными значениями. Возможно, придется об этом договариваться с бэкендерами, возможно, подкрутить в BFF или в той тулзе с помощью которой вы ходите в API.
Словом, мы хотим, чтобы в итоге получилось как-то так
interface RequestSuccess {
status: "success";
data: string[];
}
interface RequestError {
status: "error";
message: string;
}
type RequestResult = RequestSuccess | RequestError;
Если мы добились этого, мы получили поле по которому сможем довольно легко сузить тип
function request(): RequestResult { ... }
const result = request();
if(result.status === 'error') {
result.message; // ok
} else {
result.data; // ok
}
Обратите внимание, что TS достаточно умен, чтобы по значению поля понять, к какому из частей дискриминируемого юниона на самом деле относится объект данных. То есть в ветке if тип переменной result будет определен как RequestSuccess, а в else как RequestError
Альтернативные способы
Читаемость и удобство дискриминанта в типах кажется мне весомой причиной использовать их везде где только возможно. Но а что, если сделать этого вдруг нельзя? Ну, допустим не получается добиться того чтобы в ответе появилось дополнительное поле: бэкендеры экономят байты траффика, BFF не поддается, а времени ковырять инструментарий нет.
Есть как минимум три способа. Один хуже другого, чесслово.
- Заменить интерфейсы на классы и применить
instanceof
class RequestSuccess {
data: string[] = [];
}
class RequestError {
message: string = '';
}
type RequestResult = RequestSuccess | RequestError;
const result = request();
if(result instanceof RequestError) {
result.message;
} else {
result.data;
}
Будет работать точно так же, но использовать классы только лишь для того задать тип немного претит честолюбию.
- Написать свой кастомный тайпгард
interface RequestSuccess {
data: string[];
}
interface RequestError {
message: string;
}
type RequestResult = RequestSuccess | RequestError;
function isError(response: RequestResult): response is RequestError {
return 'message' in response;
}
if(isError(result)) {
result.message;
} else {
result.data;
}
Писать тайп-гарды нужно уметь. Но часто бывает так, что это слишком много лишнего кода для решения какой-то базовой, простенькой проблемы в рамках нашей задачи. Это конечно вряд ли отпугнет вас, если вы используете Redux да еще и без тулкита, но сказать я был обязан.
- Использовать для сужения типа оператор
in
interface RequestSuccess {
data: string[];
}
interface RequestError {
message: string;
}
type RequestResult = RequestSuccess | RequestError;
const result = request();
if('message' in result) {
result.message;
} else {
result.data;
}
Это пожалуй лучшее решение из альтернативных. Нет лишних сущностей, нет лишнего кода. Дискриминейтед юниону оно проигрывает только человекочитаемостью. Грубо говоря, заходя в ветку if я понимаю что в result есть поле message, но не понимаю (или не сразу понимаю) что внутри этого блока я обрабатываю состояние ошибки. Совсем другое дело когда мы явно проверяем значение поля-дискриминанта.
Но выбирать, конечно, вам.







