Check does dataSample1
's value and dataSample2
's value satisfy to type ValidData
:
const dataSample1: unknown = {
foo: 5,
bar: "beekeeper",
baz: true,
quux: {
alpha: 5,
bravo: "PLATINUM"
}
};
const dataSample2: unknown = {
foo: -4,
bar: "abc",
quux: {
alpha: 2,
bravo: "BRONZE"
}
};
type ValidData = {
foo: number;
bar: string;
baz: boolean;
quux: {
alpha: number;
bravo: "PLATINUM" | "GOLD" | "SILVER";
};
}
Herewith:
foo
must be the non-negative integer (0, 1, 2, etc.)bar
must be the string with5
characters as minimumquux.alpha
must be the integer with minimal value3
quux.bravo
must be the string with value among"PLATINUM"
,"GOLD"
,"SILVER"
Define above requirements with RawObjectDataProcessor.ObjectDataSpecification
:
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.nonNegativeInteger
},
bar: {
type: String,
required: true,
minimalCharactersCount: 5
},
baz: {
type: Boolean,
required: true
},
quux: {
type: Object,
required: true,
properties: {
alpha: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
minimalValue: 3
},
bravo: {
type: String,
required: true,
minimalCharactersCount: 5,
allowedAlternatives: [ "PLATINUM", "GOLD", "SILVER" ]
}
}
}
}
}
Execute the data processing:
const dataSample1ProcessingResult: RawObjectDataProcessor.ProcessingResult<ValidData> = RawObjectDataProcessor.
process(dataSample1, validDataSpecification);
Check is processed data valid, and if no log all errors:
if (dataSample1ProcessingResult.rawDataIsInvalid) {
Logger.logError({
errorType: InvalidExternalDataError.NAME,
title: InvalidExternalDataError.DEFAULT_TITLE,
description: "The dataSample1 is invalid:\n" +
`${RawObjectDataProcessor.formatValidationErrorsList(dataSample1ProcessingResult.validationErrorsMessages)}`,
occurrenceLocation: "upper scope"
});
} else {
Logger.logSuccess({
title: "Processing done",
description: "The dataSample1 is valid."
});
}
To access the processed data (dataSample1ProcessingResult.processedData
) and use it, you need to make sure is
dataSample1ProcessingResult.rawDataIsInvalid
property falsy first:
if (dataSample1ProcessingResult.rawDataIsInvalid) {
// throw error or create the message and exit from current function/method
}
// Now you can assess your data
console.log(dataSample1ProcessingResult.processedData)
For the dataSample1
we'll get:
[ Success ] Processing done
The dataSample1 is valid.
For the dataSample2
, we'll get:
[ Error ] Invalid external data
The dataSample2 is invalid:
=== Error No. 1 ==========
Expected and actual numbers set mismatch
● Property / element: 'Example.foo'
This numeric value is in not member of 'non-negative integer' set as required.
● Property / element specification:
{
"type": "number",
"required": true,
"numbersSet": "NON_NEGATIVE_INTEGER"
}
● Actual value: -4
=== Error No. 2 ==========
Minimal characters count fall short
● Property / element: 'Example.bar'
This string value has 3 characters while required minimal characters count is 5.
● Property / element specification:
{
"type": "string",
"required": true,
"minimalCharactersCount": 5
}
● Actual value: abc
=== Error No. 3 ==========
Required property is missing
● Property / element: 'Example.baz'
This property has been marked as 'required' while actual value is 'undefined'.
● Property / element specification:
{
"type": "boolean",
"required": true
}
● Actual value: undefined
=== Error No. 4 ==========
Minimal value fall short
● Property / element: 'Example.quux.alpha'
This value is smaller than required minimal value 3.
● Property / element specification:
{
"type": "number",
"required": true,
"numbersSet": "ANY_INTEGER",
"minimalValue": 3
}
● Actual value: 2
=== Error No. 5 ==========
Disallowed value alternative
● Property / element: 'Example.quux.bravo'
This value is not among allowed alternatives.
● Property / element specification:
{
"type": "string",
"required": true,
"minimalCharactersCount": 5,
"allowedAlternatives": [
"PLATINUM",
"GOLD",
"SILVER"
]
}
● Actual value: BRONZE
Error type: InvalidExternalDataError
Occurrence location: upper scope
The processing of unknown at advance external data is one of the programming essentials. The external data could be:
- The data from file
- The data from HTTP request
- Raw data from the database
- The query parameters from URI
and so on.
Initially, this external data has unknown
or, what's even worse, any
type.
But in below example we are believing that data retrieved from server is matching with User
type:
type User = {
ID: string;
familyName: string;
givenName: string;
};
fetch("http://example.com/users/1").
then((response: Response): void => response.json()).
then((data: unknown) => {
const user: User = data as User;
const fullName: string = `${user.givenName} ${user.familyName}`;
console.log(fullName);
});
From the viewpoint of reality, it will not be match with expected almost in each project, especially when the client and server application parts are separated. It will be tens, hundreds and ever thousands fixed errors before retrieving data be fully match with expected.
For the retrieving of the data from the file case, the invalid data probability is extremely high
when the config file (.json
, .yaml
, .etc.) is being filling by user.
So it's required to validate the external data, and only when confirm that it is valid, assign the specific type
like User
in the example below and use it.
The Type guards is a native TypeScript conception. The Type guard is a function returning boolean value, but returning value annotation is a little unusual:
type User = {
ID: string;
familyName: string;
givenName: string;
};
function isUser(rawData: unknown): rawData is User {
return typeof rawData === "object" &&
rawData !== null &&
typeof((rawData as { ID: unknown; }).ID ) === "string" &&
typeof((rawData as { familyName: unknown; }).familyName) === "string" &&
typeof((rawData as { givenName: unknown; }).givenName) === "string";
}
The native TypeScript approaches including type guards well described in The unknown Type in TypeScript, the front end engineer Marius Schulz 's article. Here is important that:
Type guards actually does not to guarantee what the value has specified type - this is just an asking to TypeScript to believe that it is such as.
For example, the below type guard is doing the checks completely unrelated with User
type:
type User = {
ID: string;
familyName: string;
givenName: string;
};
function isUser(rawData: unknown): rawData is User {
return isArbitraryObject(rawData) &&
typeof((rawData as { title: unknown }).title) === "string" &&
typeof((rawData as { price: unknown }).price) === "number";
}
const potentialUser: unknown = { title: "Shampoo", price: 1000 };
if (isUser(potentialUser)) { // it will be truthy for "potentialUser"
console.log(potentialUser.familyName); // Of course, "undefined"
}
Why so poor? The TypeScript is being compiling to JavaScript, but neither type
s nor interface
s exists on JavaScript.
The validation is being executed when TypeScript already has been compiled to JavaScript, so no way to refer on User
type's properties names/values/types (to be more precise, TypeScript does not provide the concept such as saving the accessible
metadata of type
s and interface
s on compiled JavaScript). This is a first problem.
But there are at least two more problems:
- Second problem Ever type guard as
isUser
returnedfalse
, we will not know which property is invalid. UnlikeUser
with only three properties (ID
,familyName
,givenName
) the object type from real project could have a couple tens of properties and also nested ones. - Third problem Type guard returns
false
on first falsy condition. But there could be a multiple properties not satisfying to type guard's condition. To debug it quickly, we need to know all violations, not just first one.
Conceptually RawObjectProcessor
is a huge configurable type guard with logging a lot of additional functionality.
Therefore, it is the time to clearly state that RawObjectProcessor
does not solve the first problem:
type User = {
ID: string;
familyName: string;
givenName: string;
};
const rawData: unknown = { ID: 1, familyName: "John", title: "Shampoo" };
/* It just casts the raw data to `User` when it obeys the specified validation rules, but this validation rules could
* have a mistake or simply be unrelated with `User`. */
const processingResult: RawObjectDataProcessor.ProcessingResult<User> = RawObjectDataProcessor.process(rawData, {
nameForLogging: "User",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
ID: {
required: true,
type: String
},
familyName: {
required: true,
type: String
},
givenName: {
required: true,
type: String
}
}
});
if (processingResult.rawDataIsInvalid) {
Logger.logError({
errorType: InvalidExternalDataError.NAME,
title: InvalidExternalDataError.DEFAULT_TITLE,
description: "The raw data is invalid:\n" +
`${RawObjectDataProcessor.formatValidationErrorsList(processingResult.validationErrorsMessages)}`,
occurrenceLocation: "upper scope"
});
}
But RawObjectProcessor
solves the second problem and third problem. In the case of below example,
the errored log will be:
[ Error ] Invalid external data
The raw data is invalid:
=== Error No. 1 ==========
Expected and actual value types mismatch
● Property / element: 'User.ID'
This value must have type 'string' while actually it's type is: 'number'.
● Property / element specification:
{
"required": true,
"type": "string"
}
● Actual value: 1
=== Error No. 2 ==========
Required property is missing
● Property / element: 'User.givenName'
This property has been marked as 'required' while actual value is 'undefined'.
● Property / element specification:
{
"required": true,
"type": "string"
}
● Actual value: undefined
Error type: InvalidExternalDataError
Occurrence location: upper scope
But RawObjectDataProcessor
is not just a validator; it has some additional functionality, for example:
- Pre-validation and post-validation modifications of the properties
- Renaming of object keys
- Substitution of default values
And much more - here what is below documentation about.
As it follows from the utility name, RawObjectDataProcessor works with native JavaScript objects
(typeof rawData === "object"
). But the object has a lot of usages, subsequently, subtypes.
RawObjectDataProcessor works with below three partial cases of object
.
Fixed key and value pairs object means that object has fixed scheme.
Type User
in example below is of such subtype.
type User = {
ID: string;
familyName: string;
givenName: string;
};
Unlike fixed key and value pairs object, the keys and values in associative array are unknown at advance.
Before ES2015 (AKA ES6), the simple object could be used as associative array. In TypeScript, it could be designated as indexed type:
type Users = { [ID: string]: User | undefined };
The Map
became available from ES2015
. It could be used as associative array, but here is important that
besides string and numbers other data types also could be used as a key.
Anyway, the associative array usage of plain object is still present and will be present.
One of the reasons is JSON
data being converting to non-Map
native object.
In the indexed arrays (Array.isArray(rawData) === true
), elements are being identified by numbers (counting form 0
)
called indexes. But Array
is also an object
(same as Map
, Set
etc.).
It's important to distinct the properties and their names (keys) and values.
- The property has name (as known as key) and value.
- The key and value pair also called entry.
- The indexed array could be represented as the object with numeric keys and values of any type, but from the indexed array keys called indexes and values called elements.
- The value could be a part of object's entry or the element of an array, but it also could exist on its own, without object context.
Currently RawObjectDataProcessor.ProcessingResult.processedData
is the object/array built from zero based on raw data,
the first argument of process()
method. In means that if the raw data has some function properties, or getters/setters
or some properties has not been declared in second parameter, these properties will be lost.
For instance, in below example the method incrementBar
will not present on processingResult.processedData
:
const rawData: unknown = {
foo: "ALPHA",
bar: 1,
incrementBar(): void {
this.bar++;
}
};
type ValidData = {
foo: string;
bar: number;
incrementBar(): void;
};
const processingResult: RawObjectDataProcessor.ProcessingResult<ValidData> = RawObjectDataProcessor.
process(rawData, {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.nonNegativeInteger
},
bar: {
type: String,
required: true,
minimalCharactersCount: 5
},
}
});
if (processingResult.rawDataIsInvalid) {
// ...
return;
}
console.log(processingResult.processedData.incrementBar) // -> undefined
For the retrieving of the data on frontend side via AJAX or retrieving the data from the file, basically this limitation does not cause trouble. However, there are some cases when all properties must be kept.
The adding of data processing without creating of new object is on plans. However, according to preliminary estimates, the volume of the code can increase by 2 times, so the priority of this task will depend on demand.
The minimal code consists from:
- The type declaration of desired value. Yor are free to use
type
orinterface
depending on your guidelines. - The calling of
RawObjectDataProcessor.process()
with assigment of the returnable value to variable of typeRawObjectDataProcessor.ProcessingResult
. - The handling of invalid data
The RawObjectDataProcessor.ProcessingResult
is a generic and it's single parameter is the type of expected data which
must be declared on step 1.
/* Step 1 */
type Sample = { foo: string };
/* Step 2 */
const dataProcessingResult: RawObjectDataProcessor.ProcessingResult<Sample> = RawObjectDataProcessor.
process(
{ foo: "ALPHA" },
{
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: String,
required: true
}
}
}
);
/* Step 3 */
if (dataProcessingResult.rawDataIsInvalid) {
Logger.logError({
errorType: InvalidExternalDataError.NAME,
title: InvalidExternalDataError.DEFAULT_TITLE,
description: "The sample data is invalid:\n" +
`${RawObjectDataProcessor.formatValidationErrorsList(dataProcessingResult.validationErrorsMessages)}`,
occurrenceLocation: "upper scope"
});
return;
}
Once step 3 will done, you can access to processed data via dataProcessingResult.processedData
.
Use prod live template available with IntelliJ IDEA official plugin to instantly input the initial code.
Normally, the parameters of RawObjectDataProcessor.process
are being extracted to dedicated variables:
const rawData: unknown = { foo: "ALPHA" };
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: String,
required: true
}
}
};
const dataProcessingResult: RawObjectDataProcessor.ProcessingResult<Sample> = RawObjectDataProcessor.
process(rawData, validDataSpecification)
In the real project, the rawData
's value will be taken from the external data source live HTTP request/response or the file.
This section if focusing on the values by themselves, are they the elements of array or the values of the objects. Although the fixed key and value pairs type object case is good for examples, the knowledge of this section could be used for elements of indexed arrays and values of associative arrays.
The type check is required for each object property or array element.
Currently, the RawObjectDataProcessor
can check below values' types:
- Numbers: designated as
Number
orRawObjectDataProcessor.ValuesTypesIDs.number
- Strings: designated as
String
orRawObjectDataProcessor.ValuesTypesIDs.string
- Boolean: designated as
Boolean
orRawObjectDataProcessor.ValuesTypesIDs.boolean
- Object (nested): designated as
Object
orRawObjectDataProcessor.ValuesTypesIDs.fixedKeyAndValuePairsObject
- Indexed array of uniform elements: designated as
Array
orRawObjectDataProcessor.ValuesTypesIDs.associativeArrayOfUniformTypeValues
- Associative array of uniform elements: designated as
Map
, but this notation is conditional because the value actually not the instance ofMap
, just a plain object, so it is recommended to useRawObjectDataProcessor.ValuesTypesIDs.associativeArrayOfUniformTypeValues
.
Also, it's possible to allow to have two or more types (RawObjectDataProcessor.ValuesTypesIDs.oneOf
).
For each property or array element specification, set type
with one above values:
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.nonNegativeInteger,
minimalValue: 8
},
bar: {
type: String,
required: true,
minimalCharactersCount: 5
},
baz: {
type: Boolean,
required: true
},
quux: {
type: Object,
required: true,
properties: {
alpha: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
minimalValue: -2
},
bravo: {
type: String,
required: true,
minimalCharactersCount: 5,
allowedAlternatives: [ "PLATINUM", "GOLD", "SILVER" ]
}
}
},
bat: {
type: Array,
required: true,
element: {
type: String,
minimalCharactersCount: 1
}
},
xyzzy: {
type: RawObjectDataProcessor.ValuesTypesIDs.associativeArrayOfUniformTypeValues,
required: true,
value: {
type: String
}
},
plugh: {
type: RawObjectDataProcessor.ValuesTypesIDs.oneOf,
required: true,
alternatives: [
{
type: Number,
numbersSet: RawObjectDataProcessor.NumbersSets.decimalFractionOfAnySign
},
{
type: String,
minimalCharactersCount: 1
}
]
}
}
}
If the value has been specified as Number
/RawObjectDataProcessor.ValuesTypesIDs.number
, below options are available.
Because this option is very important, it is required (must be specified).
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.nonNegativeInteger
}
}
};
Available number sets are:
- RawObjectDataProcessor.NumbersSets.naturalNumber
- 1, 2, 3 and so on towards infinity.
- RawObjectDataProcessor.NumbersSets.nonNegativeInteger
- All naturals numbers and also 0 which is the positive number according to Math.
- RawObjectDataProcessor.NumbersSets.negativeInteger
- -1, -2, -3 and so on towards minus infinity.
- RawObjectDataProcessor.NumbersSets.negativeIntegerOrZero
- Negative integers and also 0
- RawObjectDataProcessor.NumbersSets.anyInteger
- All natural numbers, negative integers and also 0
- RawObjectDataProcessor.NumbersSets.positiveDecimalFraction
- The fraction of "[integerPart].[decimalPart]" type, e. g. "3.62", herewith greater than 0.
- RawObjectDataProcessor.NumbersSets.negativeDecimalFraction
- The fraction of `-[integerPart.[decimalPart]`, e. g. `-4.62`, herewith less than 0.
- RawObjectDataProcessor.NumbersSets.decimalFractionOfAnySign
- Any of positive or negative decimal fraction
- RawObjectDataProcessor.NumbersSets.anyRealNumber
- The integer or decimal fraction of any sign
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.nonNegativeInteger,
minimalValue: 3,
maximalValue: 10
}
}
};
If you want to allow just specific discrete numeric values (e. g. 3
, 5
and 7
), specify it via allowedAlternatives
:
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Number,
required: true,
numbersSet: RawObjectDataProcessor.NumbersSets.nonNegativeInteger,
allowedAlternatives: [ 3, 5, 7 ]
}
}
};
If the value has been specified as String
/RawObjectDataProcessor.ValuesTypesIDs.string
, below options are available.
If the value must be the member of specific enumeration, this option is what you need.
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: String,
required: true,
allowedAlternatives: [ "BRONZE", "SILVER", "GOLD" ]
}
}
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: String,
required: true,
minimalCharactersCount: 1,
maximalCharactersCount: 127
}
}
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
ID: {
type: String,
required: true,
fixedCharactersCount: 32
}
}
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
ID: {
type: String,
required: true,
validValueRegularExpression: /^prefix-.+$/u
}
}
};
If the value has been specified as Boolean
/RawObjectDataProcessor.ValuesTypesIDs.boolean
, below options are available.
Intended to be used in the cases like:
- The value must be either
true
orundefined
(is this case,required: false
option also must be specified) - The value must be either
false
ornull
(in this caserequired: true;
,nullable: true
option also must be specified) - The value must be either string or
false
(in this case,RawObjectDataProcessor.ValuesTypesIDs.oneOf
is required instead ofBoolean
/RawObjectDataProcessor.ValuesTypesIDs.boolean
).
If the value has been specified as Object
/RawObjectDataProcessor.ValuesTypesIDs.fixedKeyAndValuePairsObject
, it's
required to enumerate the specification of this nested object's properties:
type ValidData = {
alpha1: {
alpha2: number;
bravo2: string;
};
};
const dataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "ValidData",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
alpha1: {
required: true,
type: Object,
// ↓ Valid nested object properties specification
properties: {
alpha2: {
required: true,
type: Number,
numbersSet: RawObjectDataProcessor.NumbersSets.anyRealNumber
},
bravo2: {
required: true,
type: String
}
}
}
}
};
If the value has been specified as Array
/RawObjectDataProcessor.ValuesTypesIDs.indexedArrayOfUniformElements
,
it's required specify the element
property:
type ValidData = {
foo: Array<{ bar: string; baz: boolean; }>;
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: Array,
required: true,
// ↓ Valid array element specification
element: {
type: Object,
properties: {
bar: {
type: String,
required: true,
minimalCharactersCount: 5
},
baz: {
type: Boolean,
required: true
}
}
}
}
}
};
Also, below options are available.
- minimalElementsCount
- The integer representing required minimal elements count.
- maximalElementsCount
- The integer representing allowed maximal elements count.
- exactElementsCount
- The integer representing required exact elements count. Incompatible with two previous options.
- allowUndefinedTypeElements
-
Set this option to
true
to allow the empty array elements. Please note that this option still does not allow thenull
elements. - allowNullElements
- Set this option to
true
to allow thenull
elements.
If the value has been specified as Map
/RawObjectDataProcessor.ValuesTypesIDs.associativeArrayOfUniformTypeValues
,
it's required specify the value
property:
type ValidData = {
foo: { [key: string]: string; };
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
type: RawObjectDataProcessor.ValuesTypesIDs.associativeArrayOfUniformTypeValues,
required: true,
value: {
type: String,
minimalCharactersCount: 1
}
}
}
};
Also, below options are available.
- minimalEntriesCount
- The integer representing required minimal entries (key and value pairs) count.
- maximalEntriesCount
- The integer representing allowed maximal entries (key and value pairs) count.
- exactEntriesCount
- The integer representing required exact entries (key and value pairs) count. Incompatible with two previous options.
- requiredKeys
- The array of string specifying the keys which respcetive values must present.
- allowedKeys
- The array of string specifying which keys are allowed.
- keyRenamings
-
The object of
{ [rawKey: string]: string; }
shape (actually the associative array, too) defining the new names of specific keys. - allowUndefinedTypeValues
-
Set this option to
true
to allow the explicit undefined values. Please note that this option still does not allow thenull
value. - allowNullValues
- Set this option to
true
to allow thenull
elements.
Each property must be...
- either explicitly marked as required by
required: true
, - either marked as conditionally required by
requiredIf
(see conditional requirement subsection), - either have default value by
defaultValue
- or explicitly marked as optional by
required: false
.
Please note that requirement conception in RawObjectDataProcessor is not related with null
values.
type ValidData = {
foo: string;
hoge: string;
} & (
{
bar: string;
baz: string;
} | {
bar?: undefined;
baz?: string;
}
);
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Example",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
// Required
foo: {
type: String,
required: true
},
// Optional
bar: {
type: String,
required: false
},
// Conditionally required
baz: {
type: String,
requiredIf: {
predicate: (rawData__currentObjectDepth: ArbitraryObject): boolean => isString(rawData__currentObjectDepth.bar),
descriptionForLogging: "'bar' is presents"
}
},
// Has default value
hoge: {
type: String,
defaultValue: "ALPHA"
}
}
}
Conditional requirement means that some property must present or could not be present conditionally.
For example, in below type swimmingPoolDepth__meters
must present if hasSwimmingPool
is true
:
type Villa = {
floorsCount: number;
totalSquare__squareMeters: number;
hasSwimmingPool: boolean;
swimmingPoolDepth__meters?: number;
}
From the viewpoint of TypeScript, swimmingPoolDepth__meters
is not conditionally required.
The conditional requirement declaration in TypeScript is limited. For this case, we can declare
type Villa = {
floorsCount: number;
totalSquare__squareMeters: number;
} & (
{
hasSwimmingPool: true;
swimmingPoolDepth__meters: number;
} | {
hasSwimmingPool?: false;
}
);
Please note that |
is not the exclusive OR and there is no exclusive OR operation for type aliases.
Sometimes it matters.
To define the conditional requirement in PropertiesSpecification
, you need to specify requredIf
with two required
options: predicate
and descriptionForLogging
:
type PropertyRequirementCondition = {
readonly predicate: (rawData__currentObjectDepth: ArbitraryObject, rawData__full: ArbitraryObject) => boolean;
readonly descriptionForLogging: string;
};
- If the
predicate
will return true, the property will be considered as required. - If you have nested objects and the condition should refer the parent objects, you will need the second parameter of
predicate,
rawData__full
because firstOne -rawData__currentObjectDepth
- returns just object of current depth level. - In
descriptionForLogging
, define verbally when predicate returnstrue
for clear logging. For the above example withVilla
, thedescriptionForLogging
could be like "'hasSwimmingPool' is true".
type Villa = {
floorsCount: number;
totalSquare__squareMeters: number;
} & (
{
hasSwimmingPool: true;
swimmingPoolDepth__meters: number;
} | {
hasSwimmingPool?: false;
}
);
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Villa",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
hasSwimmingPool: {
type: Boolean,
required: false
},
baz: {
type: String,
requiredIf: {
predicate: (rawData__currentObjectDepth: ArbitraryObject): boolean =>
rawData__currentObjectDepth.hasSwimmingPool === true,
descriptionForLogging: "'hasSwimmingPool' is true"
}
},
// ...
}
};
- If some property could be the
null
, specifynullable: true
- If you want to replace
null
with some other value, specify:nullSubstitution
with the same type astype
. - If you want to replace
null
withundefined
, specifypreValidationModifications: [ nullToUndefined ]
. In this case, the property will be validated according the property requirement conception.
Please note than required: false
does not allow the null
values - nullable: true
has been designed for this.
type Sample = {
foo: string | null;
bar?: string | null;
baz?: string;
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Sample",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
// ↓ 'null' is allowed while 'undefined' is not.
foo: {
type: String,
required: true,
nullable: true
},
// ↓ both 'null' and 'undefined' allowed
bar: {
type: String,
required: false,
nullable: true
},
// ↓ 'null' will be transformed to 'undefined' before validation
baz: {
preValidationModifications: nullToUndefined,
type: String,
required: false
}
}
};
Specify newName
to rename the property's key during processing.
type Sample = {
alpha: string;
};
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "Sample",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
foo: {
newName: "alpha",
type: String,
required: true,
}
}
}
All public methods are static.
Main method of the class representing most functionality. Processing the raw data (first argument) according it's specification (second parameter) and options (third parameter, optional).
process<ProcessedData extends ReadonlyParsedJSON, InterimValidData extends ReadonlyParsedJSON = ProcessedData>(
rawData: unknown,
validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification,
options: RawObjectDataProcessor.Options = {}
): RawObjectDataProcessor.ProcessingResult<ProcessedData>
- Generic parameters
- ProcessedData - refers to desired type. Since RawObjectDataProcessor is for objects only, the ProcessedData must be the subtype of ReadonlyParsedJSON.
- InterimValidData - optional parameter actual only whe postProcessing property of third parameter of object-type has been specified with the function.
- Parameters
- rawData Unknown as advance data which usually being retrieved from external source like HTTP or file.
- validDataSpecification The object of type RawObjectDataProcessor.ObjectDataSpecification describing what raw data should it be, and, optionally, some extra processings of one or more properties of raw data. Must corresponding to specified ProcessedData, but TypeScript can not inspect is this really the case.
- options
- postProcessing the function accepting the InterimValidData and returning ProcessedData, both of which are generic parameters of the process method.
- localization the object of RawObjectDataProcessor.Localization type containing the function generating the validation error messages.
- Returned value is the object containing the processing result. The property processedData is available is and only if property rawDataIsInvalid is truthy, same as validationErrorsMessages is available if and only if property rawDataIsInvalid is falsy. So, you need to check rawDataIsInvalid before access to processedData or rawDataIsInvalid.
The section getting-started contains the example with most of above properties.
formatValidationErrorsList(
messages: Array<string>, localization: RawObjectDataProcessor.Localization = RawObjectDataProcessor.defaultLocalization
): string
Formatting the validation errors messages which RawObjectDataProcessor.ProcessingResult contains when raw data is invalid.
if (processingResult.rawDataIsInvalid) {
Logger.logError({
errorType: InvalidExternalDataError.NAME,
title: InvalidExternalDataError.DEFAULT_TITLE,
description: "The raw data is invalid:\n" +
`${RawObjectDataProcessor.formatValidationErrorsList(processingResult.validationErrorsMessages)}`,
occurrenceLocation: "upper scope"
});
}
setDefaultLocalization(newLocalization: RawObjectDataProcessor.Localization): void
Changing the default language of errors messages. The RawObjectDataProcessor.Localization is pretty big object containing the text data and template functions for each error message.
Officially, Japanese and Russian localization are available. You can create your ows localization object of RawObjectDataProcessor.Localization type. Check the listing of English localization as reference.
getNormalizedValueTypeID(
valueType: NumberConstructor |
StringConstructor |
BooleanConstructor |
ObjectConstructor |
ArrayConstructor |
MapConstructor |
RawObjectDataProcessor.ValuesTypesIDs
): RawObjectDataProcessor.ValuesTypesIDs
The method exclusively for localization. Transforms String (the StringConstructor) to RawObjectDataProcessor.ValuesTypesIDs.string, Number (the NumberConstructor) to RawObjectDataProcessor.ValuesTypesIDs.number etc. and always returns the element of RawObjectDataProcessor.ValuesTypesIDs enumerations.
Basically, the switch/case could detect StringConstructor, NumberConstructor etc., but under a certain combination of conditions it does not work:
So the transformations like StringConstructor to RawObjectDataProcessor.ValuesTypesIDs.string aare more complicated than just switch/case, that is what getNormalizedValueTypeID method is doing.
The second parameter of RawObjectDataProcessor.process
method.
Will be used in error message which will be generated if raw data is invalid.
It should make sense which exactly data does not match with expected. For example:
- If it is some data retrieved via HTTP, the name like UsersListRetrieving.ResponseData makes sense. Although in this example Pascal case has been used, this name could be the normal text.
- If it is the data from some configuration file, the name like NNNConfigFile (where NNN is the application name) makes sense.
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "UsersListRetrieving.ResponseData",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
/* Properties' specification goes here ... */
}
}
Defines which subtype of object must be the raw data. Object-type data classification has been considered in corresponding section of theoretical minimum chapter.
- If you have specified
subtype: ObjectSubtypes.fixedKeyAndValuePairsObject
, you must defineproperties
property
with specification of each property of target object (see PropertiesSpecification and related - Object properties specification). - If you have specified
subtype: ObjectSubtypes.indexedArray
, you must defineelement
property with specification of target indexed array's element. - If you have specified
subtype: ObjectSubtypes.associativeArray
, you must definevalue
property with specification of target associative array's value.
const validDataSpecification: RawObjectDataProcessor.ObjectDataSpecification = {
nameForLogging: "UsersListRetrieving.ResponseData",
subtype: RawObjectDataProcessor.ObjectSubtypes.fixedKeyAndValuePairsObject,
properties: {
/* Properties' specification goes here ... */
},
// OR
subtype: RawObjectDataProcessor.ObjectSubtypes.indexedArray,
element: {
/* Element's specification goes here ... */
},
// OR
subtype: RawObjectDataProcessor.ObjectSubtypes.associativeArray,
value: {
/* Values's specification goes here ... */
},
}
The PropertiesSpecification is the associative where key is a name of expected property:
export type PropertiesSpecification = { readonly [propertyName: string]: CertainPropertySpecification; };
The CertainPropertySpecification is one of:
- NumberPropertySpecification
- StringPropertySpecification
- BooleanPropertySpecification
- NestedObjectPropertySpecification
- NestedUniformElementsIndexedArrayPropertySpecification
- NestedUniformElementsAssociativeArrayPropertySpecification
- MultipleTypesAllowedPropertySpecification
As has been explained in Theoretical-minimum chapter, the value could exist inside object as property, but also could be independent (until global scope). Thus, each of above extends the specification of respective value:
- NumberValueSpecification - subset of NumberPropertySpecification
- StringValueSpecification - subset of StringPropertySpecification
- BooleanValueSpecification - subset of BooleanPropertySpecification
- FixedKeyAndValuePairsObjectValueSpecification - subset of NestedObjectPropertySpecification
- UniformElementsIndexedArrayValueSpecification - subset of NestedUniformElementsIndexedArrayPropertySpecification
- UniformElementsAssociativeArrayValueSpecification - subset of NestedUniformElementsAssociativeArrayPropertySpecification
- MultipleTypesAllowedValueSpecification - subset of MultipleTypesAllowedPropertySpecification
Even with first generic parameter of process method - the shape of valid and processed data.
- rawDataIsInvalid - boolean property reflects the result of validation of the raw data.
- processedData - the validated and processed data. Presents (non-undefined) if and only if rawDataIsInvalid is true.
- validationErrorsMessages - the array of validation errors messages. Presents (non-undefined) if and only if rawDataIsInvalid is false.