Comments 15
Когда реальный ответ прилетает с сервера — никакой проверки не происходит. TypeScript просто верит вашей аннотации и молчит.
Ну нет же. В этот момент уже никакого TypeScript там нет. Он исчез во время компиляции.
Возможно не правильно сформулировал, я просто хотел подсветить здесь, что в данном моменте - типизация != гарантия прихода данных того типа что мы указали.
Typescript не врёт, а выполняет свою функцию. То что вы подумали о том, что язык отвечает за данные, то это никогда и нигде так не было. Всё что приходит снаружи подлежит или валидации, или доверию. Язык может лишь подсветить проблемы при правильном использовании, но ни в одном даже очень типизированном языке, как правило, не убирают возможно “небезопасно” скастовать тип. Так это не язык врёт, это программист его неправильно использует
Ой да, полностью согласна! Я раньше думала, что раз TypeScript показывает типы, то всё ок, а потом поймала баг из-за того, что сервер прислал совсем не то, что ожидалось. Теперь всегда делаю проверку через Zod реально экономит кучу нервов.
В обозримом будущем планирую выпустить новую библиотеку, которая немного с другой стороны подходит к валидации, чем Zod, Valibot и другие schema-first решения.
В ней TS типы запроса и ответа по эндпоинту остаются в текущем виде и являются полноценным источником правды:
import type { TypeUser } from '@models/TypeUser';
type TypeRequest = { id: string; };
type TypeResponse = TypeUser;А библиотека из этого файла генерирует валидаторы, которые можно вызвать в рантайме:
export default {
request: { id: 'string' },
response: {
id: 'number',
name: 'string',
email: 'string',
},
} as const;Соответственно, остается встроить проверки в функцию вызова апи
import schema from 'validatiors/api/getUser.ts'
async function getUser(request: TypeRequest): TypeResponse {
// удаляет все лишнее
const reqErrors = check({
schema: schema.request,
value: request,
getExtraneous: console.log
});
if (reqErrors) throw new Error(reqErrors);
const response = await fetch('/api/user').json();
// удаляет все лишнее
const resErrors = check({
schema: schema.response,
value: response,
getExtraneous: console.log
});
if (resErrors) throw new Error(resErrors);
// TypeScript верит вам на слово,
// но валидаторы не пропустят расхождений по типам и структуре данных
return response;
};Фронт ясно говорит, что ему нужны только 3 поля в TypeUser, поэтому check очистит легаси, приходящее с бэка, специфику для других платформ, дубликаты с разным неймингом.
getUser вернет 100% соответствующую типам структуру и залогирует в консоль все расхождения в ожиданиях, а при критичных - вернет список понятных ошибок.
Поддерживать TS-типы как источник правды намного проще (они все равно нужны в приложении), чем описывать схемы в Zod-стиле.
Поддерживать TS-типы как источник правды намного проще (они все равно нужны в приложении), чем описывать схемы в Zod-стиле.
Но схемы умеют делать дополнительные проверки, например по регексу.
А вообще, имхо, источником правды должен быть сваггер/openApi, из которого на фронте генерятся типы и (при необходимости) валидаторы.
Zod - инструмент с широким назначением, может и формы валидировать, и проверять данные на email, длину, маски, использоваться на бэке и в пайплайнах нормализации.
Но на фронте задача другая - если использовать TS, то важна именно совместимость по типам - что пришла строка, а не число (и неважно - это email или isoDate), так обеспечивается работоспособность рантайма. А проверки на email в основном нужны бэку при сохранении данных (ну и для форм если надо - можно zod или аналог прикрутить).
При этом намного удобнее не полностью Swagger Open API переносить в проект, а детально заводить типы - если фронту в TypeUser нужно только 3 поля, а не 800 присылаемых бэком с relationships, ios / android / tv спецификой, то лучше так и завести. Это же потом и стабильность улучшит - не будет ломаться при изменении неиспользуемых полей или ошибок в них, фронтендер всегда может сказать "мы используем 3 поля, проверьте не устарела ли выдача из 800 полей", трафика меньше, тестировать проще, в сторы фронта не протекает лишнего и т.п. Думаю любой найдет много причин держать код и рантайм чистыми, а не "тащить все что дают".
Продолжу мысль, с Вашего позволения: … а сваггер/openAPI схемы должны генериться из TypeScript типов :) Логично же?
Оно у вас будет работать с типами вида user: { gender: 'F', childrenCount: number } | { gender: 'M', salary: number }?

Да, там делаю очень замороченный TS AST парсер с поддержкой всего, что может быть в Open API, включая реэкспорты, алиасы, построение инверсированных зависимостей (дерева импортов). Конкретно для этого кейса будет такой валидатор
user: [
'union',
{ gender: ['literal', 'F'], childrenCount: 'number' },
{ gender: ['literal', 'M'], salary: 'number' }
]И если бэк пришлет { gender: 'M', salary: '100' } то будет ошибка response.user is none of 2 types. Для дебага должно быть достаточно.
Ну и в любом случае это лучше, чем ловить где-то при открытии модалки undefined has no method toFixed in user.salary.toFixed()
В угоду обратной совместимости, многие вещи в тайпскрипте возвращают any, вместо нормального типа. Если бы Response.json() возвращал unknown, то подобной проблемы бы не возникло и программисту пришлось бы разбираться в чём дело(конечно, многие бы просто забили и скастили бы тип к нужному, но всё же).
Есть маленькие библиотеки, которые исправляют эти косяки, например ts-reset, с ними жить становится чуть безопаснее.
Zod позволяет описать схему один раз — и получить сразу и тип, и рантайм-валидацию:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>; // тип выводится автоматически
const getUser = async (): Promise<User> => {
const res = await fetch('/api/user');
const data = await res.json();
return UserSchema.parse(data); // если структура не та — бросит ошибку
};Лично для меня этот подход крайне спорный, и я его не использую из следующих соображений:
Данные пришедшие с сервера по определению валидны, так как они проходят валидацию при сохранении. Нам не нужно проверять что поле
emailэто правда емэйл. Да – правда. Его бы не занесли в базу будь он не валидным.Проверять нужно только соответствие контракту: ждали строку – пришла строка, ждали свойство
userName– пришлоuserName, а неuser_name.Можно валидировать данные перед отправкой, и для этого я использую Constraint Validation –
min,max,patternи пр.Мне не нужны какие-то непонятные схемы, мне нужна конкретная модель сущнсти, и я предпочитаю rich models.
И для этого я написал себе библиотеку, которая решает мои задачи. То что вы называете схемой, у меня – модель:
import { Transformer } from 'kr-transformer';
class UserModel {
// Тип – строка
name = '';
// Тип – число
age = 0;
// тип – булево
student = false;
setName() {
// ...какой-то метод
}
get someComputation() {
return this.age * 3;
}
}
const json1 = { name: 'John', age: 42, student: true };
const json2 = { name: 'John', age: "42", student: true };
const json3 = { name: 'John', age: 42, student: null };
const json4 = { username: 'John', age: 42, student: false };
try {
// валидный JSON, ошибок нет
const model = Transformer.fromJSON(json1, UserModel);
} catch(error) {
console.log(error) //
}
try {
// age строка – невалидно
const model = Transformer.fromJSON(json2, UserModel);
} catch(error) {
console.log(error) // Unexpected type of <age>, expect Number but receive String
}
try {
// student null – невалидно
const model = Transformer.fromJSON(json3, UserModel);
} catch(error) {
console.log(error) // ...ошибка
}
try {
// username вместо name – невалидно
const model = Transformer.fromJSON(json4, UserModel);
} catch(error) {
console.log(error) // ...ошибка
}То есть, полученный с сервера невалидный JSON, просто превратится в модель с понятной ошибкой. Модель сама по себе тип. С моделью дальше гораздо удобнее работать чем с каким-то json-ом.
Важное напоминание, спасибо за статью.
При актуальности проблемы я бы посоветовал разработчикам завести себе BFF, там можно сконцентрировать эти проверки и код для них предотвратив размазывание по проекту.
TypeScript врёт — а вы об этом не знаете. Валидация ответов сервера