ts-basis
v5.4.21
Published
TS Basis is a general toolbox library based on Typescript for augmenting the standard library offered by browsers and other JS runtimes like Node.js.
Downloads
1,626
Readme
TS Basis
TS Basis is a general toolbox library based on Typescript for augmenting the standard library offered by browsers and other JS runtimes like Node.js.
General Structure
TS Basis library is designed to help programmers add useful runtime features for class instances and data models.
import { defineOn } from 'ts-basis';
class TargetClass {
constructor(init?: Partial<TargetClass>) {
defineOn( this, TargetClass, lib => {
lib.extension1.featureA( /* define */ )
...
lib.extension2.featureX( /* define */ )
}
Feature Extensions
Validatable
(Runtime data model validation)Ephemerals
(Non-serializable properties)Derivables
(Derived properties)
Base Classes
TypeToolsBase
(Base interface for all extensions)DataImportable
(Instance initialization from object initializers)PropertiesController
(Controller class for getters & setters)
Util Classes
ClassLineage
(Inheritance utils)
Validatable
- Runtime Data Model Validation
While using interfaces as data model definitions type-guides programmers during development, it does not support robust runtime enforcement on instantiated models. Ideally, data validation should accompany the data definition itself; however, native data modeling with Typescript interfaces often forces programmers to scatter validation logic elsewhere in the codebase.
An initializable class
as the data model definition instead of using interface
can assist greatly in pinpointing the source of bad data on runtime as well as keeping validation definitions nicely in the class definition itself. (** The only cavest is the performance overhead, which is relatively negligible unless you are dealing with millions of objects. See Performance Characteristics section for more details).
import { Validatable, defineOn } from 'ts-basis';
class MyModel {
strVal = 'test';
numVal = 5;
dateVal = null;
constructor(init?: Partial<MyModel>) {
defineOn( this, MyModel, lib => {
lib.validatable.enforce( this, { init }, {
strVal (value, e) { // throw on bad assignment
if ( typeof value !== 'string' || ! value.startsWith('test') ) {
return e.throw(`${e.path} must be a truthy string starting with 'test'`); }},
numVal (value, e) { // don't throw; just ignore bad assignments
if ( typeof v !== 'number' ) {
return e.cancel(); }}, // equivalently: return false;
dateVal (value, e) { // hijack & transform assigment value
if ( value === 'transformMe' ) {
return e.transformValue(new Date()); }},
});
});
}
}
// Generic object testing against a given type (true/false)
const result = Validatable.test({ strVal: 'bad' }, MyModel); // false
// Runtime Instantiation Guard
const inst1 = new MyModel(); // valid; default prop1='test' and prop2=5 are valid.
const inst2 = new MyModel({ strVal: 'test2' }) // valid; starts with 'test'
const inst3 = new MyModel({ strVal: 'yolo', numVal: 100 }); // throws; 'yolo' does not start with 'test'
// Runtime Property Assignment Guard
inst1.strVal = 'a'; // throws; and assignment cancels
inst1.numVal = 'string'; // canceled ("ignored"); will remain value 5
inst1.dateVal = 'transformMe'; // transformed; inst1.dateVal will be new Date() instance.
// External Data Casting
const inst1_1 = Validatable.cast(MyModel, { numVal: 10, dateVal: null }); // valid; new MyModel instance { strVal: 'test', numVal: 10, dateVal: null }
const inst1_2 = Validatable.cast(MyModel, { strVal: 'bad' }); // throws
const inst1_3 = Validatable.cast(MyModel, { strVal: 'bad' }, false); // returns null; throwError=false
Ephemerals
- Serialization Control
Ephemerals
extension will modify toJSON
function of the class to make sure ephemeral properties are not serialized by JSON.stringify
. (Performance cost: negligible.)
class EphemTest {
keptProp = 'value';
ignoredProp1 = 'test5'; /* EPHEM */
ignoredProp2 = 5; /* EPHEM */
constructor(init?: Partial<EphemTest>) {
defineOn(this, EphemTest, lib => {
lib.ephemerals.of(this, {
ignoredProp1: true,
ignoredProp2: true,
...
}
}
console.log( JSON.stringify(new EphemTest()) ); // {"keptProp":"value"}
Derivables
- Derived Properties
Derivables
extension will listen to source properties changes and update the derived property's value. Usually, derived properties are also non-serializable (since it can be derived from data), therefore you can apply Ephemerals
on it.
class DerivedTestClass {
strVal = 'string';
numVal = 5;
derivedProp: string; /* DERIVED */
derivedProp2: string; /* DERIVED EPHEM */
constructor(init?: Partial<DerivedTestClass>) {
defineOn(this, DerivedTestClass, lib => {
lib.derivables.of(this, { /* options */ }, {
derivedProp: (strVal, numVal) => strVal + numVal, // short format
derivedProp2: { // long format, functionally the same as above (a little fatster in terms of perf)
from: { strVal:1, numVal:1 },
derive() { return this.strVal + this.numVal; }
}
...
lib.ephemerals.of(this, { derivedProp2: true, ...
}
}
const d = new DerivedTestClass({ strVal: 'joined', numVal: 5 }));
console.log( d.derivedProp ); // 'joined5'
d.strVal = 'joined2_'; // updated source
d.numVal = 10;
console.log( d.derivedProp ); // 'joinedUpdated_10' automatically updated
console.log( JSON.stringify(d) ); // derivedProp2 not saved: {"strVal":"joined2_","numVal":10,"derivedProp":"joined2_10"}
PropertiesController
- Getters/Setters
Most extensions are built on the base class PropertiesController
which does a lot more general handling of getters, setters, and onvaluechage events.
import { defineOn } from 'ts-basis';
class MyClass {
myProp1 = 'firstValue';
myProp2 = 300;
constructor(init?: Partial<MyClass>) {
defineOn(this, MyClass, lib => {
const manageOptions = {}; // options like 'prepend', 'alwaysFront', 'alwaysBack', 'order'
lib.propertiesController.manage(this, manageOptions, {
myProp1: {
set(value, e) { console.log(`setter: ${e.path} being set '${value}'`); },
get(value, e) { console.log(`getter: ${e.path} being accessed`); },
change(oldValue, newValue, e) { console.log(`onchange: ${e.path} changed from '${oldValue}' to '${newValue}'`); },
}
});
// manage function is additive in terms of handlers
lib.propertiesController.manage(this, manageOptions, {
myProp1: {
set(value, e) { console.log(`setter 2`); },
get(value, e) { console.log(`getter 2`); },
change(oldValue, newValue, e) { console.log(`onchange 2`); },
}
});
}
}
const a = new MyClass();
a.myProp1 = a.myProp1 + '2';
// getter: MyClass.myProp1 being accessed
// getter 2
// setter: MyClass.myProp1 being set 'firstValue2'
// setter 2
// onchange: MyClass.myProp1 changed from 'firstValue' to 'firstValue2'
// onchange 2
ClassLineage
- Inheritance Util
ClassLineage.of
fetches top-down inheritance chain of given class instance or class itself.
import { ClassLineage } from 'ts-basis';
class A {}
class B extends A {}
...
class Z extends Y {}
const a = new A();
const b = new B();
// Class itself
ClassLineage.of(A); // [A]
ClassLineage.of(B); // [A, B]
// Or its class instances
ClassLineage.of(a); // [A]
ClassLineage.of(b); // [A, B]
ClassLineage.namesOf(B); // ['A', 'B']
ClassLineage.namesOf(b); // ['A', 'B']
// Parent of
ClassLineage.parentOf(B); // A
ClassLineage.parentNameOf(B); // 'A'
ClassLineage.parentOf(A); // null; A doesn't extend from anything
ClassLineage.parentNameOf(A); // null
// Bottom-up order
ClassLineage.of(Z, false); // [Z, Y, ..., A]
ClassLineage.namesOf(Z, false); // ['Z', 'Y', ..., 'A']
// Ancestry
ClassLineage.lastCommonAncestor(A, B); // A
ClassLineage.commonAncestorInfo(A, B); // { commonAncestors: [ A ], lastCommonAncestor: A, senior: A, junior: B, distance: 1, travel: 0, levelCompare: 1, levelDifference: 1 }
class Unrelated {}
ClassLineage.commonAncestorInfo(A, Unrelated); // { commonAncestors: [], lastCommonAncestor: null, senior: null, junior: null, distance: Infinity, travel: Infinity, levelCompare: NaN, levelDifference: NaN }
class B2 extends A {}
ClassLineage.areRelated(B, B2); // true; common parent A
ClassLineage.areRelated(B, Unrelated); // false; no common ancestor
Performance: all ClassLineage
methods are about 10M/s; results are auto-cached since types don't generally change during runtime. To turn it off, set ClassLineage.noCache
to true.
Advanced Topics
Validatable
Inheritance Control
Building onto Parent Class Validations
class MyModel2 extends MyModel {
constructor(init?: Partial<MyModel2>) {
super(); // inherits all definitions from parent
defineOn( this, MyModel2, lib => {
lib.validatable.enforce( this, { init }, {
strVal (value, e) { // add additional validation on strVal
if ( !value.endsWith('_ending') ) {
return e.throw(`${e.path} must end with '_ending'`);
...
}
}
const a = new MyModel(); // valid; MyModel only requires startsWith('test')
const b = new MyModel2({ strVal: 'test_ending' }); // valid, starts with 'test' and ends with '_ending'
const a = new MyModel2(); // throws; NOTE: default initializer strVal 'test' defined in MyModel fails at the new constraint endsWith('_ending')
Selectively Overriding Parent Class Validations
beforeSuper
to execute before parent constructor to register a preceding handler- and control flow the definitions by:
e.stopPropagation()
to stop the event chain early ORe.ignoreDefinitionsFrom(...Classes)
to selectively ignore amongst stacked handlers
import { defineOn, beforeSuper, beforeDefinitionOf, superArgs } from 'ts-basis';
// Inheritance flow:
// MyModel -> MyModel2 -> MyOverridingModel
class MyOverridingModel extends MyModel2 {
newProp: string = '';
constructor(init?: Partial<TestClassModified2>) {
super(beforeSuper(() => {
beforeDefinitionOf(MyModel, instance => {
defineOn(instance, MyOverridingModel, lib => {
lib.validatable.enforce(instance, { /* no init */ }, {
strVal(value, e) {
e.ignoreDefinitionsFrom(MyModel);
// moving forward, ignore strVal handler from MyModel,
// but still use validations defined in MyModel2
// OR
e.stopPropagation(); // this makes this handler the only active validation for strVal
/* do validations here; will execute before MyModel's handlers */
// ...
beforeDefinitionOf(MyModel2, instance => {
/* Some logic to override MyModel2 */
});
}, superArgs( /* no init */ ) ));
// To add validations for new members,
// and finish with initializing from 'init'
defineOn(this, MyOverridingModel, lib => {
lib.validatable.enforce(this, { init }, {
strVal (value, e) { /* reachable if stopPropagation hasn't been called */ }
newProp (value, e) { /* validate newly added class member */ }
});
});
}
}
const a = new MyOverridingModel({ strVal: '_ending' });
// valid; no longer has to start with 'test' (MyModel validation ignored)
// but still has to end with _ending (defined in MyModel2)
Validatable
Performance Characteristics
Baseline (on ~2.5 Ghz core, slower as more complex validations added):
- Good data:
- Instantiation: 100k/s (e.g.
let a = new MyModel(data);
) - Property set: 2.5M/s (e.g.
a.prop = b
)
- Instantiation: 100k/s (e.g.
- Bad data:
- If using try/catch block (expensive):
- Instantiation: 50k/s, Property set: 250k/s
- Using
Validatable.errorsOf
to detect fault (See next section)- Instantiation: 100k/s, Property set: 1.5M/s
- If using try/catch block (expensive):
TypeTools.test(obj, MyModel)
1 M/s (good data), 500k/s (bad data)
If you've called TypeTools.config.disableExtensions(Validatable)
and are manually validating, instantiation and property set are both within around 5M ~ 50M/s
Performance Optimization
The slowest part of validatable class is the instantiation (due to registration overhead of TS Basis extensions) and try/catch block (creating new error and throwing is pretty expensive because of stack tracing overhead.)
Performance can be greatly helped by:
- If validity checking is all you need, use
Validatable.test(obj, MyModel)
- Running with
TypeTools.config.disableThrow()
and manually checking
import { defineOn, TypeTools, Validatable, ... } from 'ts-basis';
const obj = { strVal: 'test', numVal: 100 };
// 1) If validity checking is all you need,
let valid: boolean;
valid = Validatable.test(obj, MyModel); // relatively inexpensive.
valid = TypeTools.test(obj, MyModel); // TypeTools.test is an alias of Validatable.test
// 2) Opt in for manually checking errors instead of throwing.
TypeTools.config.disableThrow();
const inst = new MyModel(obj); // would throw normally but doesn't throw.
inst.strVal = 'invalid'; // would throw normally but doesn't throw.
inst.numVal = 'not a number'; // cancels assignment.
valid = Validatable.resultOf(a); // false; inst has 2 errors and 1 cancel.
const instErrors = Validatable.errorsOf(a);
if (instErrors.length > 0) { /* some properties called e.throw */
for (const tracer of instErrors) {
console.log(`${tracer.e.path} has errored with ${tracer.trace.message}, stack: ${tracer.trace.stack}`);
// [0] = strVal ERROR at `new MyModel(obj)` where strVal = 'test' is executed.
// [1] = strVal ERROR at `inst.strVal = 'invalid';`
}
}
const instCancels = Validatable.cancelsOf(a);
if (aCancels.length > 0) { /* some properties called e.cancel */
for (const tracer of instCancels) {
console.log(`${tracer.e.path} has errored with ${tracer.trace.message}, stack: ${tracer.trace.stack}`);
// [0] = numVal assignment CANCEL on instantiation at `inst.numVal = 'not a number'`
}
}
License MIT
Copyright (c) 2024 Justin Pauli ([email protected])
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.