typescript-object-helper
v2.1.4
Published
A simple dependency to aid in mapping and instantiating typescript classes
Downloads
7
Maintainers
Readme
Typescript Object Helper
This is a TypeScript module that provides utility functions for creating and copying class objects from JSON. The module is to be intended for use in situations where a JSON object needs to be instantiated into a target class.
Note: TOH 2.1.0 requires RxJS7 or greater.
Benefits:
- No need for writing long, repetitive, difficult to maintain constructors.
- Unlike interfaces, classes have functions such as getters, setters, custom functions.
- Objects can be checked against their class type with the "instanceof" operator.
- Typescript Object Helper functions can be used to improve data integrity at runtime, by instantiating properties to default values if the API fails to provide a response that matches the front end model.
- Avoids the horrible practice of casting json to target classes and mistakenly believing they are of that type:
exampleApiGetFunction(): Observable<ExampleClass> {
return this.http.get('/api/example/endpoint')
.pipe(
instantiateFromJsonOperator(ExampleClass)
);
}
this.exampleApiGetFunction().subscribe(exampleResult => {
console.log(exampleResult instanceof ExampleClass)// true
exampleResult.classFunction() // works
console.log(exampleResult.imbeddedClass instanceof ImbeddedClass)// true
})
exampleApiGetFunctionWithoutTSOH(): Observable<ExampleClass> {
return this.http.get<ExampleClass>('/api/example/endpoint');
}
this.exampleApiGetFunction().subscribe(exampleResult => {
console.log(exampleResult instanceof ExampleClass)// false
exampleResult.classFunction() // errors
console.log(exampleResult.imbeddedClass instanceof ImbeddedClass)// false
})
Functions
instantiateFromJson(json, Class)
creates an instance of a class from a JSON object, regardless of it being an array or object.
instantiateFromJsonReverseParam(Class, json)
a reverse parameter version of instantiateFromJson.
copyArray(jsonArray, Class)
creates an array of instances of a class from an array of JSON objects.
copyObject(jsonObject, Class)
creates an instance of a class from a JSON object.
ClassCast Decorator
The ClassCast function is a decorator that can be used to add typing information for nested or deep objects and arrays.
Example Usage
export class DeeplyNestedClass {
public deeplyNestedField: string | undefined = undefined;
public deeplyNestedMethod(): string {
return 'Hola Mundo';
}
}
class NestedClass {
public nestedField: string | undefined = undefined;
public nestedField2: string = 'Default nested Value'
@ClassCast(DeeplyNestedClass)
public deeplyNestedClass: DeeplyNestedClass | undefined = undefined;
@ClassCast(DeeplyNestedClass)
public deeplyNestedArray: DeeplyNestedClass[] = [];
public nestedMethod(): string {
return 'Hello World';
}
}
class MyClass {
public field: string | undefined = undefined;
public field2: number | undefined = undefined;
public field3: string = 'Default String';
@ClassCast(NestedClass)
public nestedClass: NestedClass | undefined = undefined;
@ClassCast(NestedClass)
public nestedArray: NestedClass[] = [];
}
const json = {
field: 'value',
field2: 123,
nestedClass: {
nestedField: 'nested value',
deeplyNestedClass: {
deeplyNestedField: 'deeply nested value'
}
},
nestedArray: [
{
nestedField: 'nested value 1',
deeplyNestedClass: {
deeplyNestedField: 'deeply nested value 1'
}
},
{
nestedField: 'nested value 2',
deeplyNestedClass: {
deeplyNestedField: 'deeply nested value 2'
}
}
]
};
const instantiatedClass = copyObject(json, MyClass);
console.log(json.nestedArray[1].deeplyNestedClass instanceof DeeplyNestedClass) // Output: false
console.log(instantiatedClass.nestedArray[1].deeplyNestedClass instanceof DeeplyNestedClass) // Output: true
console.log(instantiatedClass.nestedArray[1].deeplyNestedClass.deeplyNestedMethod()) // Output: 'Hola Mundo'
export class TestClass extends ObjectHelperBaseObject {
_myPrivateValue: string | undefined = undefined; // Will still be treated as private by object helpers
private _myValue: string | undefined = undefined; // Will still be treated as private by object helpers
mySecondValue: string = 'default value'
private notActuallyPrivate: string = 'test'; // Will not be treated as private by object helpers
private _combinedValue: string | undefined = undefined; // Will be treated as private by object helpers and typescript
// nestedTestClass: TestClass = new TestClass(); // Don't do this. It will be infinitely recursed
nestedInnerTestClass: TestInnerClass = new TestInnerClass();
// @ts-ignore
thisPropertyWillNotExist: string // Will not be part of the resulting object because no initial value
onInit() {
this._combinedValue = this.myValue + ' ' + this.mySecondValue
}
set myValue(value: string | undefined) {
this._myValue = value;
}
get myValue(): string | undefined {
return this._myValue
}
get firstLetterOfMyValue() {
if (this._myValue) {
return this._myValue[0]
} else {
return '';
}
}
get combinedValue() {
return this._combinedValue;
}
}
export class TestInnerClass extends ObjectHelperBaseObject {
myInnerValue: string | undefined = 'test';
}
Here are some example tests
const json = {
_myPrivateValue: 'will not be set',
mySecondValue: 'will be set',
notActuallyPrivate: 'will still be set',
myValue: 'my value',
_combinedValue: 'will not be set'
}
it('Object should be properly initialized', () => {
const instantiatedTestObject: TestClass = copyObject(json, TestClass);
expect(instantiatedTestObject).toEqual(jasmine.any(TestClass));
expect(instantiatedTestObject._myPrivateValue).toEqual(undefined);
expect(instantiatedTestObject.mySecondValue).toEqual('will be set');
expect(instantiatedTestObject.firstLetterOfMyValue).toEqual('m');
expect(instantiatedTestObject.combinedValue).toEqual(json.myValue + ' ' + json.mySecondValue);
expect(instantiatedTestObject.nestedInnerTestClass).toEqual(jasmine.any(TestInnerClass));
expect(instantiatedTestObject.hasOwnProperty('thisPropertyWillNotExist')).toBe(false);
});
ObjectHelperBaseObject
The ObjectHelperBaseObject is an abstract class that may be extended by classes intending on being instantiated by object helper functions. This class currently contains two useful functions.
onInit()
Object helper functions will actually run the "onInit" function of any class after all other fields and setters have set, regardless of whether or not the class extends the ObjectHelperBaseObject. This can be very useful if you need to set or modify values after you are sure all fields have their values.
toJson()
This function will return a serialized json which will ignore private values (those beginning with '_'), and will return the current value for any getter. This is useful when serializing an object to send to an API.
Using the same TestClass and values from the example above, the json output will be:
{
"mySecondValue": "will be set",
"notActuallyPrivate": "will still be set",
"nestedInnerTestClass": "{\"myInnerValue\":\"test\"}",
"myValue": "my value",
"firstLetterOfMyValue": "m",
"combinedValue": "my value will be set"
}
This can be used as in the example below:
exampleApiPostFunction(exampleObject: ExampleClassThatExtendsBaseClass): Observable<ExampleClassThatExtendsBaseClass> {
return this.http.post('/api/example/endpoint', exampleObject.toJSON())
.pipe(
instantiateFromJsonOperator(ExampleClassThatExtendsBaseClass)
);
}
RxJS Operators
Additionally, this library includes custom RxJS operators that can be used to transform and manipulate streams of asynchronous data in RxJS Observables.
instantiateFromJsonOperator(MyClass)
This operator simply takes the target class as an argument and will return an instance of that class with the input data. Note that this only requires the argument of the class, not the input. That is automatically passed by RxJS. If your response has a wrapper or other date, simply pass a map operator first (shown in the example below).
exampleApiGetFunction(): Observable<ExampleClass> {
return this.http.get('/api/example/endpoint')
.pipe(
map(response => response.data),
instantiateFromJsonOperator(ExampleClass)
);
}
instantiateFromJsonOperator(MyClass)
This works exactly the same as instantiateFromJsonOperator, only for array. Note that you should still pass the target class without respect to the array.
exampleApiGetFunction(): Observable<ExampleClass[]> {
return this.http.get('/api/example/array')
.pipe(
map(response => response.data),
instantiateFromArrayOperator(ExampleClass)
);
}
*Note: Neither of these operators will work if you try to cast the result, as is a common (though bad) practice. I.E "".
willNotWork(): Observable<ExampleClass> {
return this.http.get<ExampleClass>('/api/example/endpoint')
.pipe(
map(response => response.data),
instantiateFromJsonOperator(ExampleClass)
);
}
deepCopy()
This operator takes an Observable source and returns a new Observable that emits deep copies of the values emitted by the source. It uses the cloneDeep function from the Lodash library to create deep copies of objects and arrays. This operator can be used to avoid issues with shared object references and mutations in complex data structures. Note that this operator takes no arguments, it will simply pass copy of the input to the next operator or subscription in the pipe.
logger()
This operator takes an optional label and additional arguments, and returns a new Observable that logs the values emitted by the source to the console. It also logs a stack trace to help with debugging. The label and arguments are used to format the log message, and the label is prefixed with "LOGGER" for easy identification. This operator can be used to add debugging information to an Observable pipeline.
Example 1: Basic logging
import { of } from 'rxjs';
import { logger } from 'typescript-object-helper/rxjs-operators';
const source$ = of('apple', 'banana', 'cherry');
source$.pipe(
logger()
).subscribe();
Output:
LOGGER: apple
Error: (Not really, just a stack trace...)
<stack trace with line number>
LOGGER: banana
Error: (Not really, just a stack trace...)
<stack trace with line number>
LOGGER: cherry
Error: (Not really, just a stack trace...)
<stack trace with line number>
Example 2: Logging with a label
import { of } from 'rxjs';
import { logger } from 'typescript-object-helper/rxjs-operators';
const source$ = of('apple', 'banana', 'cherry');
source$.pipe(
logger('fruits')
).subscribe();
Output:
LOGGER - (fruits): apple
Error: (Not really, just a stack trace...)
<stack trace with line number>
LOGGER - (fruits): banana
Error: (Not really, just a stack trace...)
<stack trace with line number>
LOGGER - (fruits): cherry
Error: (Not really, just a stack trace...)
<stack trace with line number>
addIncrementer()
This operator returns a function that takes an Observable source and returns a new Observable that emits tuples containing an incrementing counter and the values emitted by the source. The counter is incremented for each value emitted by the source, and the resulting tuple is emitted as a new value. This operator can be used to add sequence numbers or other identifiers to an Observable pipeline.
Example:
import { of } from 'rxjs';
import { addIncrementer } from 'typescript-object-helper/rxjs-operators';
const source$ = of('apple', 'banana', 'cherry');
source$.pipe(
addIncrementer()
).subscribe(([count, value]: [number, string]) => {
console.log(`${count} - ${value}`);
});
Output:
1 - apple
2 - banana
3 - cherry
Notes:
For Object Helper Functions to work properly, every property of the object must be initialized. This can be with an explicit undefined, null, empty array, default value, new Class().
For Object Helper Functions to work properly, the default constructor must be a no-args constructor.
Recommendation: secondary constructor type functions should return an instance initialized by the copyObject function.
static secondaryConstructorFromSomeInterfaceOrClass(myInput: someInterfaceOrClass): MyClass {
return copyObject({
field1: someInterfaceOrClass.field1Value,
field2: someInterfaceOrClass.field2Value,
nestedClass: someInterfaceOrClass.nestedClassAsInterface
}, MyClass);
}
Object helper functions will not treat anything beginning with '_' as private and will not apply values to them directly.
For now, object helper functions will add "extra values" in the input object to the output object. This can be overridden by passing a third argument, "allowExtras" - a boolean - to copyObject. In future versions the default will be changed to false.
const obj1 = copyObject({extraField: someVal}, MyClass);
obj1.hasOwnProperty('extraField') // true
const obj2 = copyObject({extraField: someVal}, MyClass, false);
obj1.hasOwnProperty('extraField') // false