RawObjectDataProcessor — Демо
Допустим, значение параметра externalData получено из внешнего источника данных, недосягаемого для проверки типов с помощью TypeScript.
function onDataRetrieved(externalData: unknown): void {
// ...
}
В частности, это могут быть следующие сценарии:
- Полученные данных с клиентской части при клиент-серверном взаимодействии
- Наоборот, полученные данных с серверной части при клиент-серверном взаимодействии
- Получение данных из базы данных
- Чтение данных из файла (JSON, YAML и подобных)
Мы ожидаем, что полученные данные будут иметь тип 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 до тех пор, пока не докажем TypeScript-у, что свойство isInvalid имеет значение false:
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 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",
isNaN_Forbidden: true
}
● 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 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 [ 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 [ 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",
isNaN_Forbidden: true
}
● 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",
isNaN_Forbidden: true,
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:87:11)
at <anonymous> (D:\Demo.ts:99:1)
at Object.<anonymous> (D:\Demo.ts:106: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 («валидатор сырах объектных данных»)? Потому что он может не только валидировать данные, но и вносить в них изменения, в частности переименовывать ключи и менять значения. Всё это и многое другое описано далее в настоящей документации.