Deep-fallback makes work with fallbacks enjoyable. Deep-fallback is a tiny library, that removes the handling of fallbacks and facilitates their usage on nested objects.
Install using npm:
npm install --save deep-fallback
or using yarn
yarn add deep-fallback
How many times have you used nested default parameters in functions? Or maybe you often work with nested objects, checking for every possible property to be set? Or you might be a fan of ||
operator when it comes to default values? Then, this library is your choice. The idea behind the library is to simplify the usage of fallbacks on nested objects through proxying. This allows us to get rid of verbose ways of setting default values and make it more enjoyable. You also gain more control over the data flow with the options provided by you.
import createFallback from 'deep-fallback';
const thing = { a: 1 };
const defaultThing = { b: 2 };
const fallback = createFallback(thing, defaultThing);
console.log(fallback.a); // <~ 1
console.log(fallback.b); // <~ 2
import createFallback from 'deep-fallback';
const thing = { a: { b: { c: 1 } } };
const defaultThing = { a: { b: { y: 2 } } };
const fallback = createFallback(thing, defaultThing);
console.log(fallback.a.b.c); // <~ 1
console.log(fallback.a.b.y); // <~ 2
import createFallback from 'deep-fallback'
const thing = { a: 1 };
const defaultThings = [{ b: 2 }, { c: 3 }];
const fallback = createFallback.many(thing, defaultThings);
console.log(fallback.a); // <~ 1
console.log(fallback.b); // <~ 2
console.log(fallback.c); // <~ 3
import createFallback from 'deep-fallback';
const thing = { a: 1 };
const fallback = createFallback({ b: 2 }, { c: 3 });
const myFallback = createFallback(thing, fallback);
console.log(myFallback.a); // <~ 1
console.log(myFallback.b); // <~ 2
console.log(myFallback.c); // <~ 3
Creates deep fallback for objects. createFallback
wraps a target in a Proxy
and returns it. The rule of thumb here: target and fallback must be objects, so we can wrap it in Proxy. This rule can be partially bypassed with options.fallbackImmediately
.
First, createFallback
wraps a target
in Proxy and listens to property access. When you access a property and it exists on the target
, we return that property. If the property does not exist on the target
, we will look for the same property path on the fallback
. If it exists on the fallback
, we return it. If it does not, undefined
will be returned.
We will continue wrapping properties you access, until we encounter a primitive value which cannot be wrapped in Proxy.
By default, if a property does not exist or is set to undefined
, fallback will be triggered. This can be overridden using options.shouldFallback
.
Is an object to perform fallback on. If a primitive is passed, it will be returned immediately and fallback will not work. This can be partially bypassed with options.fallbackImmediately
.
A value, which will be used as a fallback. In case, if a target
does not have a property you want to access, we will look for the same property path on a fallback
. If the fallback
has that property, it will be returned, otherwise undefined
will be returned.
A boolean value, which defines whether fallback will be immediately applied or not. If set to true
, in the moment of createFallback
being called options.shouldFallback
will be called with a target
immediately. It may be useful when you are not sure if a target
exists, and if it does not, we can use a fallback
value instead. Default value is set to false
.
import createFallback from 'deep-fallback'
const fallback = createFallback(undefined, { a: 1 });
console.log(fallback); // <~ undefined
const fallbackValue = { b: 2 };
const options = { fallbackImmediately: true };
const fallback = createFallback(undefined, fallbackValue, options);
console.log(fallback); // <~ proxied `fallbackValue`
A function, which decides whether fallback should be applied or not. It will be called whenever you get a property. By default, it will fallback if the value
is set to undefined
or does not exist.
Note: If a value
is primitive, and you return false
, which tells us to use the value
as the next target
. Then, the target
will be returned without proxying, because it is not an object, and fallback will not work.
import createFallback from 'deep-fallback';
const thing = { a: 1 };
const defaultThing = { a: 2 };
const options = {
shouldFallback(value) {
if (value === 1) {
return true;
}
return value === undefined;
}
};
const fallback = createFallback(thing, defaultThing, options);
// Returns 2, because we forbid any value which equals to 1 to be used
console.log(fallback.a); // <~ 2
Arguments
value: unknown - A value of the property you want to access.
target: any - A target, from which we try to access the property.
path: (string | symbol | number)[] - The full path which you access on the target.
Returns: boolean
If true
is returned, then we will continue looking for the next valid fallback by calling options.shouldFallback
. If false
is returned, then the target
which options.shouldFallback
was called with, will be used as the next target to access properties from.
A function, which returns a value, that will be returned when there are no more fallbacks left. By default, returns undefined
.
Note: noFallbackValue
will not be called if a value cannot be proxied because it is primitive.
import createFallback from 'deep-fallback';
const thing = { a: 1 };
const defaultThing = { b: 2 };
const options = {
noFallbackValue() {
return 'Not found';
}
};
const fallback = createFallback(thing, defaultThing, options);
// Returns 'Not found', because there is no 'c' property on the `target` and its `fallback`
console.log(fallback.c); // <~ 'Not found'
// Will NOT call `noFallbackValue`, because `fallback.b` is a primitive
// and cannot be tracked by Proxy
console.log(fallback.b.nope); // <~ undefined
Arguments
path: (string | symbol | number)[] - The full path which you access on the target.
target: any - The first target, from which fallback was initiated.
value: unknown - A value from the first target, from which fallback was initiated.
Returns: any
A function, which will be called when there are no more fallbacks left. This is useful for debugging purposes, to catch incorrect access to a property.
Note: onNoFallback
will not be called if a value cannot be proxied because it is a primitive.
import createFallback from 'deep-fallback'
const thing = { a: 1 };
const defaultThing = { b: 2 };
const options = {
onNoFallback(path, target) {
console.warn(`Path "${path.join('.')}" is not found on a target and its fallbacks`, target);
}
};
const fallback = createFallback(thing, defaultThing, options);
// Will force `onNoFallback` to be called to notify a user that the path cannot be found
console.log(fallback.c);
// Will NOT call `onNoFallback`, because `fallback.b` is a primitive and cannot be tracked by Proxy
console.log(fallback.b.nope);
Arguments
path: (string | symbol | number)[] - The full path which you access on the target.
target: any - The first target, from which fallback was initiated.
value: unknown - A value from the first target, from which fallback was initiated.
Returns: void
Works the same way as createFallback
, but fallbacks
parameter takes an array of fallbacks, not a single fallback.
fallbacks
array starts from high-priority fallbacks and ends with low-priority ones. In the example below, when we get fallback.c
property, chain of fallbacks will look like:
{ a: 1 } -> { b: 2 } -> { c: 3 }
import createFallback from 'deep-fallback';
const thing = { a: 1 };
const defaultThings = [{ b: 2 }, { c: 3 }];
const fallback = createFallback.many(thing, defaultThings);
console.log(fallback.a); // <~ 1
console.log(fallback.b); // <~ 2
console.log(fallback.c); // <~ 3
To make the fallback mechanism working, we wrap a target in Proxy. And every time, you get a non-primitive value from a fallback, we also wrap it in Proxy to make deep fallback possible. But this strategy leads us to a comparison issue.
import createFallback from 'deep-fallback'
const thing = { a: { b: 1 } };
const defaultThing = { c: { y: 2 } };
const fallback = createFallback(thing, defaultThing);
// `fallback` always equals to itself
console.log(fallback === fallback); // <~ true
// Because `a` property is an object, it will be dynamically wrapped in `Proxy` when you access it
// which means a reference to `fallback.a` will always be different
console.log(fallback.a === fallback.a); // <~ false, oops!
// `fallback.a` accessed only once and stored in a variable, so it equals to itself
const a = fallback.a;
console.log(a === a); // <~ true
// Value of `fallback.a.b` is primitive, and cannot be wrapper in Proxy.
// So the primitive value is returned and they are equal
console.log(fallback.a.b === fallback.a.b); // <~ true
Deep-fallback does provide Typescript support. However, there are too many dynamic options and it seems to be incredibly hard if not impossible to encode correctly with TypeScript. Often it may be necessary to force a certain type using as unknown as <type>
.
Max Kanaradze