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
},
bar: {
type: String,
isUndefinedForbidden: true,
isNullForbidden: true,
minimalCharactersCount: 5
},
baz: {
type: Boolean,
isUndefinedForbidden: true,
isNullForbidden: true
},
hoge: {
type: Number,
isUndefinedForbidden: false,
isNullForbidden: true,
numbersSet: RawObjectDataProcessor.NumbersSets.positiveIntegerOrZero
},
fuga: {
type: Number,
isUndefinedForbidden: true,
isNullForbidden: false,
numbersSet: RawObjectDataProcessor.NumbersSets.positiveIntegerOrZero
},
quux: {
type: Object,
isUndefinedForbidden: true,
isNullForbidden: true,
properties: {
alpha: {
type: Number,
isUndefinedForbidden: true,
isNullForbidden: true,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
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? Потому что он может не только валидировать данные, но и вносить в них изменения, в частности переименовывать ключи и менять значения. Всё это и многое другое описано далее в настоящей документации.