r-assign

Object.assign() with super powers

Usage no npm install needed!

<script type="module">
  import rAssign from 'https://cdn.skypack.dev/r-assign';
</script>

README

r-assign

npm version coveralls npm downloads npm types node version license

Object.assign() with a transforming feature for filtering, mapping, and validating object properties.

r-assign has its own schema declaration based on the concept of TypeScript type guards, it is composed of pure functions that can be used separately from r-assign.

When to use r-assign?

  • When needing to apply filtering or mapping of object properties
  • When dealing with data from untrustworthy sources, to validate and parse them
  • When providing a configurable interface with strict constraints
  • When doing complex transformations in objects with interdependent properties
  • When reinforcing TypeScript declarations in runtime

Reasons to use r-assign:

  • Functional approach
  • Modular, import only what you need without bloating your project
  • TypeScript support
  • Expressive in defining complex schemas
  • Flexible to create custom transforming functional
  • Helpful at preventing undesired values (like NaN when working with numbers)

Table of contents

Install

npm i r-assign

To take advantage of all TypeScript features in r-assign use latest TypeScript versions and configure the compiler options with strict and exactOptionalPropertyTypes enabled. exactOptionalPropertyTypes option is required for the usage of isOptional() type guard, if the option is not enabled the isOptionalUndefined() type guard should be used instead.

ESM import

import rAssign from 'r-assign';

CJS require

const rAssign = require('r-assign');

Usage

type TransformSchema<T = any> = {
    [key: string]: (value: any, key: string, source: any) => T;
};

rAssign(schema: TransformSchema, ...sources: any[]): any;

// Note: The actual TypeScript definition is a bit more complex. Here is
//       displayed a simplified definition for an easier understanding of the
//       function signature and schema structure.

r-assign exports a function that accepts a schema object as the first argument that defines the structure of the result by merging and transforming of any source objects provided as the following arguments.

The schema argument is a user-defined object with properties as functions that return a value based on the provided input from 3 arguments:

(value: any, key: string, source: any) => any

  • the value from the source
  • the key of the value
  • the source itself

The returned value from the function will be part of the resulting object, undefined values are skipped. The r-assign function will throw when any of the provided functions from the schema throws.

A very first example

import rAssign from 'r-assign';

const someUnknownObject = { /* ... any structure here ... */ };

const objectABC = rAssign({
    abc: (value) => {

        // Check the type of the provided value
        if (typeof value === 'string') {

            return value; // Return the value itself   --------------------+
        }                                                               // |
                                                                        // |
        return 'abc'; // Return a default value        --------------------+
    }                                                                   // |
}, someUnknownObject);                                                  // |
                                                                        // |
/*                                                                         |
objectABC: {      Add the output of the function to the resulting object   |
    abc: 'abc' <-----------------------------------------------------------+
};
*/

Here is a very simple example of the basics of what r-assign can do. The functions output provided in the schema is added to the resulting object. As the schema is composed only of functions it can define very complex and flexible functional. There are utility functions to simplify this example to the following:

import rAssign from 'r-assign';
import { getType } from 'r-assign/lib/get-type';
import { isString } from 'r-assign/lib/string';

const someUnknownObject = { /* ... any structure here ... */ };

const objectABC = rAssign({
    // Will return a string or the default value of "abc"
    abc: getType(isString, 'abc')
}, someUnknownObject);

Import utility functions

// Import all the needed functions from "r-assign/lib" submodule
import { getType, isNumber, isString } from 'r-assign/lib';

// Or import functions from specific submodule
import { getType } from 'r-assign/lib/get-type';
import { isNumber } from 'r-assign/lib/number';
import { isString } from 'r-assign/lib/string';

All utility functions are available for importing from r-assign/lib. To prevent importing all the library it is recommended to import just the specific submodules that will be used. The list of the available submodules is the following:

Primitives:

  • r-assign/lib/any - for any values
  • r-assign/lib/bigint - for bigint values
  • r-assign/lib/boolean - for boolean values
  • r-assign/lib/number - for number values
  • r-assign/lib/string - for string values
  • r-assign/lib/symbol - for symbol values
  • r-assign/lib/null - for null and nullable values
  • r-assign/lib/undefined - for undefined value
  • r-assign/lib/literal - for literal values

Complex Data Types:

  • r-assign/lib/array - for array values
  • r-assign/lib/object - for object values
  • r-assign/lib/instance - for objects values of specific instance
  • r-assign/lib/tuple - for tuple values
  • r-assign/lib/function - for function values

Type Operations

  • r-assign/lib/optional - for optional types
  • r-assign/lib/union - for union types
  • r-assign/lib/intersection - for intersection of types

Transform Functions

  • r-assign/lib/get-type - get a value of the provided type or returns the default value
  • r-assign/lib/parse-type - get a value of the provide type or throws a type error

Type guards

r-assign is build around the concept of type guards which are just functions that perform a runtime check that guarantees the type of a specific value.

import { isString } from 'r-assign/lib/string';

isString('abc'); // => true

isString(42); // => false

Some type guards require configuration to be able to do more complex type checking.

import { isArrayOf } from 'r-assign/lib/array';
import { isString } from 'r-assign/lib/string';

const isArrayOfStrings = isArrayOf(isString);

isArrayOfStrings(['abc']); // => true

isArrayOfStrings([42]); // => false

The list of all type guards:

import { isAny } from 'r-assign/lib/any';
import { isBigInt } from 'r-assign/lib/bigint';
import { isBoolean } from 'r-assign/lib/boolean';
import { isNumber, isAnyNumber } from 'r-assign/lib/number';
import { isString } from 'r-assign/lib/string';
import { isSymbol } from 'r-assign/lib/symbol';
import { isNull, isNullable } from 'r-assign/lib/null';
import { isUndefined } from 'r-assign/lib/undefined';
import { isLiteral, isLiteralOf } from 'r-assign/lib/literal';
import { isArrayOf } from 'r-assign/lib/array';
import { isObjectOf, isStrictObjectOf } from 'r-assign/lib/object';
import { isInstanceOf } from 'r-assign/lib/instance';
import { isTupleOf } from 'r-assign/lib/tuple';
import { isFunction } from 'r-assign/lib/function';
import { isOptional, isOptionalUndefined } from 'r-assign/lib/optional';
import { isUnionOf } from 'r-assign/lib/union';
import { isIntersectionOf } from 'r-assign/lib/intersection';

isAny('abc'); // => true
isAny(); // => true, will always return true

isBigInt(42n); // => true
isBoolean(false); // => true
isNumber(42); // => true
isNumber(NaN); // => false, will return true only for finite number values
isAnyNumber(NaN); // => true, will return true for any number values
isString('abc'); // => true
isSymbol(Symbol('abc')); // => true

isNull(null); // => true

// Takes as argument a type guard
const isStringOrNull = isNullable(isString);

isStringOrNull('abc'); // => true
isStringOrNull(null); // => true

isUndefined(); // => true
isUndefined(undefined); => true

// Takes as argument any primitive value or null
const isLiteralABC = isLiteral('abc');

isLiteralABC('abc'); // => true
isLiteralABC('def'); // => false

// Takes as argument an array of primitive values, at least 2 are required
const isOneOrTwo = isLiteralOf([1, 2]);

isOneOrTwo(1); // => true
isOneOrTwo(2); // => true
isOneOrTwo(3); // => false

// Takes as argument a type guard
const isArrayOfStrings = isArrayOf(isString);

isArrayOfStrings(['abc']); // => true
isArrayOfStrings(['abc',,'def']); // => false, will check for sparse arrays

// Takes as argument an object with type guards as properties
const isObjectOfNameAge = isObjectOf({
    name: isString,
    age: isNumber
});

isObjectOfNameAge({
    name: 'John',
    age: 22,
    active: true
}); // => true, checks only if the provided properties are available

// Takes as argument an object with type guards as properties
const isStrictObjectOfNameAge = isStrictObjectOf({
    name: isString,
    age: isNumber
});

isStrictObjectOfNameAge({
    name: 'John',
    age: 22,
    active: true
}); // => false, checks for the provided properties and object shape to match

// Takes as argument a class constructor
const isDate = isInstanceOf(Date);

isDate(new Date()); // => true

// Takes as argument an array of type guards
const isTupleOfStrings = isTupleOf([isString, isString]);

isTupleOfStrings(['abc', 'def']); // => true
isTupleOfStrings(['abc']); // => false

// Takes two arguments, the function arguments tuple type guards and function
// return type guard
const isStringFunction = isFunction([], isString);

isStringFunction(() => null); // => true, will check just if the provided value
                              //    is a function
isStringFunction(() => ''); // => true

// Note: Function type guards should be used as part of parsing or getting
//       values to validate the function arguments and return type, in other
//       cases they will just check for function type

// Takes as argument a type guard
const isOptionalString = isOptional(isString);

isOptionalString('abc'); // => true
isOptionalString(); // => true, will return true for string or undefined values

// Works like "isOptional()" type guard but has a different semantic when used
// in object or tuple type guards
const isOptionalUndefinedString = isOptionalUndefined(isString);

isOptionalUndefinedString('abc'); // => true
isOptionalUndefinedString(); // => true, will return true for string or undefined values

// Note: Optional type guards should be used as part of object or tuple type
//       guards, they do not have a specific semantic by themselves

const isTupleOfOptionalStrings = isTupleOf([isString, isOptional(isString)]);

isTupleOfOptionalStrings(['abc', 'def']); // => true
isTupleOfOptionalStrings(['abc']); // => true
isTupleOfOptionalStrings(['abc', undefined]); // => false

const isTupleOfOptionalStrings2 = isTupleOf([
    isString,
    isOptionalUndefined(isString)
]);

isTupleOfOptionalStrings2(['abc', 'def']); // => true
isTupleOfOptionalStrings2(['abc']); // => true
isTupleOfOptionalStrings2(['abc', undefined]); // => true

const isObjectOfNameOptionalAge = isObjectOf({
    name: isString,
    age: isOptional(isNumber)
});

isObjectOfNameOptionalAge({
    name: 'John',
    age: 22,
}); // => true

isObjectOfNameOptionalAge({
    name: 'John'
}); // => true

isObjectOfNameOptionalAge({
    name: 'John',
    age: undefined
}); // => false

const isObjectOfNameOptionalAge2 = isObjectOf({
    name: isString,
    age: isOptionalUndefined(isNumber)
});

isObjectOfNameOptionalAge2({
    name: 'John',
    age: 22,
}); // => true

isObjectOfNameOptionalAge2({
    name: 'John'
}); // => true

isObjectOfNameOptionalAge2({
    name: 'John',
    age: undefined
}); // => true

// Takes as argument an array of type guards, at least 2 are required
const isStringOrNumber = isUnionOf([isString, isNumber]);

isStringOrNumber('abc'); // => true
isStringOrNumber(42); // => true, will return true for string or number values

// Takes as argument an array of type guards, at least 2 are required
const isObjectOfStringAndBoolean = isIntersectionOf([isObjectOf({
    s: isString
}), isObjectOf({
    b: isBoolean
})]);

isObjectOfStringAndBoolean({
    s: 'abc',
    b: true
}); // => true

Parsing data

r-assign allows parsing provided values, it will validate them based on the defined schemas, will throw an error in case they do not match or just return the value in case they match. The array and object parsing returns a deep clone of the input values, for function values will return a function wrap that will validate the input and the output on function call.

import rAssign from 'r-assign';
import { parseType } from 'r-assign/lib/parse-type';
import { isString } from 'r-assign/lib/string';

try {
    const result = rAssign({
        data: parseType(isString)
    }, { /* Unknown data */ });
} catch (error) {
    /* Process error */
}

parseType() function has to be used with the rAssign function for commodity and can be configured with type guards for more complex parsing. For proper usage parsing utility functions should be wrapped inside a try...catch statement.

Getting data

Getting data in r-assign is very similar with parsing data with the exception that getting utility functions accepts default values in case the input is not the defined type.

import rAssign from 'r-assign';
import { getType } from 'r-assign/lib/get-type';
import { isString } from 'r-assign/lib/string';

const result = rAssign({
    data: getType(isString, 'abc') // Default value will be "abc"
}, { /* Unknown data */ });

Working with TypeScript

r-assign is designed to be used in both JavaScript and TypeScript projects. The output of the rAssign function is automatically inferred, it is also possible to infer a type out of the provided schema using the InferType type that is exported from the package.

import rAssign, { InferType } from 'r-assign';
import {
    isBoolean,
    isNumber,
    isOptional,
    isString,
    getType,
    parseType
} from 'r-assign/lib';

const someSchema = {
    name: parseType(isString),
    age: getType(isNumber, 0),
    active: getType(isOptional(isBoolean), false)
};

type SomeSchema = InferType<typeof someSchema>;

// type SomeSchema = {
//     name: string;
//     age: number;
//     active?: boolean;
// };

const result = rAssign(someSchema, { /* Unknown data */ });

// typeof result
// {
//     name: string;
//     age: number;
//     active?: boolean;
// };

Mapping properties

One of the main features of r-assign is to map values, utility functions can be combined to implement this functional.

import rAssign from 'r-assign';
import { isNumber, getType } from 'r-assign/lib';

const getFiniteNumber = getType(isNumber, 0);

const incrementNumber = (value) => {

    const number = getFiniteNumber(value);

    return number + 1;
};

const result = rAssign({
    value: incrementNumber
}, { /* Unknown data */ });

Filtering properties

Filtering properties in r-assign is a side effect of the thing that only properties that are defined inside transform schema are added to the output object. Another use case of filtering would be to accept some values for a properties and reject other.

As a simple example let's exclude the number 42 from the accepted values:

import rAssign from 'r-assign';
import { isNumber, getType } from 'r-assign/lib';

const getFiniteNumber = getType(isNumber, 0);

const acceptNumbersExcept42 = (value) => {

    const number = getFiniteNumber(value);

    if (number === 42) {
        return undefined;
    }

    return number;
};

const result = rAssign({
    value: acceptNumbersExcept42
}, { /* Unknown data */ });

Complex interactions

By design r-assign can interact with multiple properties and create new ones using the source object that is accessed as the third argument in transform functions.

As a simples example let's get a port value with multiple conditions, in case there is set another property in the source object like https set to true.

import rAssign from 'r-assign';
import { isNumber, getType } from 'r-assign/lib';

const httpDefaultPort = 80;
const httpsDefaultPort = 443;
const lastReservedPort = 1023;
const getFiniteNumber = getType(isNumber, httpDefaultPort);

const getPort = (value, key, source) => {

    const port = getFiniteNumber(value);

    if (port > lastReservedPort) {
        return port;
    }

    if (source.https === true) {
        return httpsDefaultPort;
    }

    return httpDefaultPort;
};

const result = rAssign({
    port: getPort
}, { /* Unknown data */ });