npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ts-compliance

v1.0.47

Published

TypeTools is a Typescript library for providing extensible tooling runtime validations and type helpers.

Downloads

70

Readme

TypeTools

TypeTools is a Typescript library for providing extensible tooling runtime validations and type helpers.

General Structure

TypeTools library is designed to help programmers add useful runtime features for class instances and data models.

import { defineOn } from '@jovian/type-tools';
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 '@jovian/type-tools';
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 '@jovian/type-tools';
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 '@jovian/type-tools';

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

  1. beforeSuper to execute before parent constructor to register a preceding handler
  2. and control flow the definitions by:
    • e.stopPropagation() to stop the event chain early OR
    • e.ignoreDefinitionsFrom(...Classes) to selectively ignore amongst stacked handlers
import { defineOn, beforeSuper, beforeDefinitionOf, superArgs } from '@jovian/type-tools';
// 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)
  • 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
  • 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 TypeTools extensions) and try/catch block (creating new error and throwing is pretty expensive because of stack tracing overhead.)

Performance can be greatly helped by:

  1. If validity checking is all you need, use Validatable.test(obj, MyModel)
  2. Running with TypeTools.config.disableThrow() and manually checking
import { defineOn, TypeTools, Validatable, ... } from '@jovian/type-tools';

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

Copyright (c) 2020 Firestack ([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.