RawObjectDataProcessor — Демо
Допустим, значение параметра externalData получено из внешнего источника данных, недосягаемого для проверки типов с помощью TypeScript. В частности, это могут быть следующие сценарии:
- Полученные данных с клиентской части в клиент-серверном взаимодействии
- Наоборот, полученные данных с серверной части в клиент-серверном взаимодействии
- Получение данных из базы данных
- Чтение данных из файла (JSON, YAML и подобных)
function onDataRetrieved(externalData: unknown): void {
// ...
}
Мы ожидаем, что полученные данные будут иметь тип SampleType, однако какими будут эти данные в реальности при выполнении программы — на стадии написания кода мы знать не можем, поэтому параметр и имеет тип unknown, а не SampleType.
type SampleType = {
foo: number;
bar: string;
baz: boolean;
hoge?: number;
fuga: string | null;
quux: {
alpha: number;
bravo: "PLATINUM" | "GOLD" | "SILVER";
};
};
Ввиду этого, просто взять и привести тип наподобие externalData as SampleType небезопасно. Хотя в силу природы TypeScript в итоге так сделать и придётся (см. «обзор проблематики»), это должно быть чем-то подкреплено, а именно валидацией.
Решая эту задачу с помощью RawObjectDataProcessor, необходимо определить спецификацию валидных данных — по сути это объект, в котором описаны метаданные для конкретных данных, в частности ожидаемые типы каждого свойства, их обязательность и прочие ограничения. Для демонстрации, помимо указания ожидаемых типов добавлены и некоторые другие ограничения, например ограничение по количеству символов или список возможных значений. На практике, такая функциональность довольно востребована, потому она не сделает этот пример избыточным.
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedSchema,
properties: {
foo: {
type: Number,
isUndefinedForbidden: true,
isNullForbidden: true,
numbersSet: RawObjectDataProcessor.NumbersSets.positiveIntegerOrZero,
isNaN_Forbidden: true
},
bar: {
type: String,
isUndefinedForbidden: true,
isNullForbidden: true,
minimalCharactersCount: 5
},
baz: {
type: Boolean,
isUndefinedForbidden: true,
isNullForbidden: true
},
hoge: {
type: Number,
isUndefinedForbidden: false,
isNullForbidden: true,
isNaN_Forbidden: true,
numbersSet: RawObjectDataProcessor.NumbersSets.positiveIntegerOrZero
},
fuga: {
type: Number,
isUndefinedForbidden: true,
isNullForbidden: false,
numbersSet: RawObjectDataProcessor.NumbersSets.positiveIntegerOrZero,
isNaN_Forbidden: true
},
quux: {
type: Object,
isUndefinedForbidden: true,
isNullForbidden: true,
properties: {
alpha: {
type: Number,
isUndefinedForbidden: true,
isNullForbidden: true,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
isNaN_Forbidden: true,
minimalValue: 3
},
bravo: {
type: String,
isUndefinedForbidden: true,
isNullForbidden: true,
minimalCharactersCount: 5,
allowedAlternatives: [ "PLATINUM", "GOLD", "SILVER" ]
}
}
}
}
};
Когда спецификация валидных данных определена, нужно передать её через параметр статическому методу process класса RawObjectDataProcessor вместе с данными пока ещё типа unknown:
function onDataRetrieved(externalData: unknown): void {
const externalDataProcessingResult: RawObjectDataProcessor.ProcessingResult<SampleType> = RawObjectDataProcessor.
process(externalData, validDataSpecification);
}
Значение, которое возвращает метод process, имеет тип со следующим определением:
export type ProcessingResult<ProcessedData> =
Readonly<
{
isRawDataInvalid: false;
processedData: ProcessedData;
} |
{
isRawDataInvalid: true;
validationErrorsMessages: ReadonlyArray<string>;
}
>;
Особенность его том, что мы не можем обратиться к свойству processedData до тех пор, пока не проверим свойство isInvalid:
const externalDataProcessingResult: RawObjectDataProcessor.ProcessingResult<SampleType> = RawObjectDataProcessor.
process(externalData, validDataSpecification);
console.log(externalDataProcessingResult.processedData);
externalDataProcessingResult.processedData без предварительной проверки externalDataProcessingResult.isRawDataInvalid не нарушив TypeScript не получится — возникнет ошибка «TS2339: Property processedData does not exist on type ProcessingResult<SampleType>». Сообщение об ошибке неточное, потому что свойства processedData нет не у типа ProcessingResult<SampleType>, а у одного из его подтипов — у которого isRawDataInvalid: true. Соответственно, перед тем, как обращаться к свойству processedData, нужно отсеять подтип, у которого этого свойства нет. Сделать это можно с помощью обычной условной конструкции:if (externalDataProcessingResult.isRawDataInvalid) {
console.log(externalDataProcessingResult.validationErrorsMessages);
}
Если же isRawDataInvalid имеет значение true, то к processedData обратиться всё ещё нельзя, потому что его просто нет, а вместо него можно обратиться к массиву, в котором содержатся сообщения об ошибках валидации, то есть описания всех несоответствий реальных данных ожидаемым. Как на эти ошибки валидации реагировать — решает разработчик в зависимости от специфики его задачи, но обычно это оповещение пользователя и/или бросание ошибки:
if (externalDataProcessingResult.isRawDataInvalid) {
throw new InvalidExternalDataError({
mentionToExpectedData: "N External Data",
messageSpecificPart: RawObjectDataProcessor.
formatValidationErrorsList(externalDataProcessingResult.validationErrorsMessages)
});
}
Как видно из примера выше, RawObjectDataProcessor имеет также статический метод для форматирования сообщений об ошибоках валидации. Например, при вызове функции onDataRetrieved со следующими входными данными:
onDataRetrieved({
foo: -4,
bar: "abc",
quux: {
alpha: 2,
bravo: "BRONZE"
}
});
Node.js выдаст следующий лог:
InvalidExternalDataError: The data "N External Data" does not match with expected.
─── Error No. 1 ────────────────────────────────────────────────────────────────
Expected and Actual Numbers Set Mismatch [ VALIDATION_ERRORS_MESSAGES-NUMERIC_VALUE_IS_NOT_BELONG_TO_EXPECTED_NUMBERS_SET ]
● Property / Element: foo
Contrary to expectations, this numeric value is in not member of "positive integer or zero"
See documentation for details: https://ee.yamato-daiwa.com/CoreLibrary/Functionality/RawObjectDataProcessor/Children/06-ValidationIssues/RawObjectDataProcessor-ValidationIssues.english.html#VALIDATION_ERRORS_MESSAGES-NUMERIC_VALUE_IS_NOT_BELONG_TO_EXPECTED_NUMBERS_SET
● Property / Element Specification:
{
"type": "number",
"isUndefinedForbidden": true,
"isNullForbidden": true,
"numbersSet": "POSITIVE_INTEGER_OR_ZERO"
}
● Actual Value: -4
─── Error No. 2 ────────────────────────────────────────────────────────────────
Minimal Characters Count Fall Short [ VALIDATION_ERRORS_MESSAGES-CHARACTERS_COUNT_IS_LESS_THAN_REQUIRED ]
● Property / Element: bar
This string value has 3 characters while at least 5 required.
See documentation for details: https://ee.yamato-daiwa.com/CoreLibrary/Functionality/RawObjectDataProcessor/Children/06-ValidationIssues/RawObjectDataProcessor-ValidationIssues.english.html#VALIDATION_ERRORS_MESSAGES-CHARACTERS_COUNT_IS_LESS_THAN_REQUIRED
● Property / Element Specification:
{
"type": "string",
"isUndefinedForbidden": true,
"isNullForbidden": true,
"minimalCharactersCount": 5
}
● Actual Value: abc
─── Error No. 3 ────────────────────────────────────────────────────────────────
Forbidden Undefined Value Of Property/Element [ VALIDATION_ERRORS_MESSAGES-FORBIDDEN_UNDEFINED_VALUE ]
● Property / Element: baz
This property/element is not defined or have explicit `undefined` value what has been explicitly forbidden.
See documentation for details: https://ee.yamato-daiwa.com/CoreLibrary/Functionality/RawObjectDataProcessor/Children/06-ValidationIssues/RawObjectDataProcessor-ValidationIssues.english.html#VALIDATION_ERRORS_MESSAGES-FORBIDDEN_UNDEFINED_VALUE
● Property / Element Specification:
{
"type": "boolean",
"isUndefinedForbidden": true,
"isNullForbidden": true
}
● Actual Value: undefined
─── Error No. 4 ────────────────────────────────────────────────────────────────
Forbidden Undefined Value Of Property/Element [ VALIDATION_ERRORS_MESSAGES-FORBIDDEN_UNDEFINED_VALUE ]
● Property / Element: fuga
This property/element is not defined or have explicit `undefined` value what has been explicitly forbidden.
See documentation for details: https://ee.yamato-daiwa.com/CoreLibrary/Functionality/RawObjectDataProcessor/Children/06-ValidationIssues/RawObjectDataProcessor-ValidationIssues.english.html#VALIDATION_ERRORS_MESSAGES-FORBIDDEN_UNDEFINED_VALUE
● Property / Element Specification:
{
"type": "number",
"isUndefinedForbidden": true,
"isNullForbidden": false,
"numbersSet": "POSITIVE_INTEGER_OR_ZERO"
}
● Actual Value: undefined
─── Error No. 5 ────────────────────────────────────────────────────────────────
Minimal Value Fall Short [ VALIDATION_ERRORS_MESSAGES-NUMERIC_VALUE_IS_SMALLER_THAN_REQUIRED_MINIMUM ]
● Property / Element: quux.alpha
This value is smaller than required minimal value 3.
See documentation for details: https://ee.yamato-daiwa.com/CoreLibrary/Functionality/RawObjectDataProcessor/Children/06-ValidationIssues/RawObjectDataProcessor-ValidationIssues.english.html#VALIDATION_ERRORS_MESSAGES-NUMERIC_VALUE_IS_SMALLER_THAN_REQUIRED_MINIMUM
● Property / Element Specification:
{
"type": "number",
"isUndefinedForbidden": true,
"isNullForbidden": true,
"numbersSet": "ANY_INTEGER",
"minimalValue": 3
}
● Actual Value: 2
─── Error No. 6 ────────────────────────────────────────────────────────────────
Disallowed Alternative of Value [ VALIDATION_ERRORS_MESSAGES-VALUE_IS_NOT_AMONG_ALLOWED_ALTERNATIVES ]
● Property / Element: quux.bravo
This value is not among following allowed alternatives.
○ PLATINUM
○ GOLD
○ SILVER
See documentation for details: https://ee.yamato-daiwa.com/CoreLibrary/Functionality/RawObjectDataProcessor/Children/06-ValidationIssues/RawObjectDataProcessor-ValidationIssues.english.html#VALIDATION_ERRORS_MESSAGES-VALUE_IS_NOT_AMONG_ALLOWED_ALTERNATIVES
● Property / Element Specification:
{
"type": "string",
"isUndefinedForbidden": true,
"isNullForbidden": true,
"minimalCharactersCount": 5,
"allowedAlternatives": [
"PLATINUM",
"GOLD",
"SILVER"
]
}
● Actual Value: BRONZE
at onDataRetrieved (D:\Demo.ts:85:11)
at <anonymous> (D:\Demo.ts:97:1)
at Object.<anonymous> (D:\Demo.ts:104:2)
at Module._compile (node:internal/modules/cjs/loader:1730:14)
at Object.transformer (C:\Users\I\AppData\Roaming\npm\node_modules\tsx\dist\register-DpmFHar1.cjs:2:1186)
at Module.load (node:internal/modules/cjs/loader:1465:32)
at Function._load (node:internal/modules/cjs/loader:1282:12)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
at cjsLoader (node:internal/modules/esm/translators:266:5)
Node.js v22.15.0
Если же isInvalid имеет значение false, то можно обращаться к свойству processedData и пользоваться им:
if (externalDataProcessingResult.isRawDataInvalid) {
throw new InvalidExternalDataError({
mentionToExpectedData: "N External Data",
messageSpecificPart: RawObjectDataProcessor.
formatValidationErrorsList(externalDataProcessingResult.validationErrorsMessages)
});
}
console.log(externalDataProcessingResult.processedData)processedData, поскольку подтип { isRawDataInvalid: true; validationErrorsMessages: ReadonlyArray<string> } исключён бросанием ошибки внутри предшествующего if-блока. Если функция или метод, которым принадлежит приведённый выше код не обязателно должны возвращать валидные данные, то вместо бросания ошибки можно использовать ключевое слово return.Помимо объектов фиксированной структуры как в примере выше RawObjectDataProcessor может валидировать и другие подвиды объектов:
- Ассоциативные массивы — отличаются от объектов фиксированной структуры произвольным количеством пар ключ-значение, однако все значения должны подчиняться единой закономерности.
- Индексные массивы
- Кортежи — отличаются от индексных массивов фиксированным числом элементов и их порядком, при этом для каждого элемента могут быть определены особые ограничения.
Почему же класс называется RawObjectDataProcessor, а не RawObjectDataValidator? Потому что он может не только валидировать данные, но и вносить в них изменения, в частности переименовывать ключи и менять значения. Всё это и многое другое описано далее в настоящей документации.