property-tunnel
v1.2.2
Published
Library providing class/object property & function alias (aka delegate or tunnel) support.
Downloads
64
Maintainers
Readme
property-tunnel: aliases to reduce boilerplate in Typescript and Javascript
This library bores tunnels from one object property to another object property in order to create aliases. Includes a decorator for use on class properties (both instance and static).
Installation
Yarn: yarn add property-tunnel
npm: npm install --save property-tunnel
Typescript
To use @alias
you need to set experimentalDecorators: true
in your
tsconfig.json
.
Targeting ES5 and below
If you want to use @alias
to make a class iterable you'll need to set
downlevelIteration: true
in your tsconfig.json
.
Javascript
To use @alias
you need to use the transform-decorators
Babel plugin.
API
The public API is what is available directly via the 'property-tunnel' module. Everything else is subject to change. These are:
@alias(["property", "to", "alias"], { <options> })
A decorator for use on class properties that creates an alias to another property or subproperty of the same class. Works on both instance and static properties.
It takes one or two arguments: an array of property keys that designates the property to alias and an options object.
Options are described by the AliasOptions interface.
Example 1: Null-safe alias for a deeply nested property
import { alias } from "property-tunnel";
class Person {
public parents: { mother?: Person, father?: Person } = {};
public firstName: string;
public lastName: string;
// If you know that everyone who ever uses your code will be using
// Typescript you can omit the `access` option for readonly aliases
// and just use the compiler-enforced readonly keyword.
@alias(["parents", "father", "lastName"], { access: "readonly" })
public readonly maidenName?: string;
public constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
}
Example 2: Using an alias to make a class iterable through a property
import { alias } from "property-tunnel";
class Worksheet {
public title: string;
public constructor(title: string) {
this.title = title;
}
// ...the rest of the class...
}
class Workbook {
public sheets: Worksheet[] = [];
private lastUntitledNum = 0;
public constructor() {
// Create a default sheet titled "Untitled 0"
this.createSheet();
}
public createSheet(title?: string): Worksheet {
if (!title) title = `Untitled ${this.lastUntitledNum++}`;
const sheet = new Worksheet(title);
this.sheets.push(sheet);
return sheet;
}
// Implement the iterable protocol through aliasing.
@alias(["sheets", Symbol.iterator])
public [Symbol.iterator]: () => Iterator<Worksheet>;
}
const workbook = new Workbook();
// Add a second sheet (see constructor)
workbook.createSheet("My Sheet");
for (let sheet of workbook) {
console.log(sheet.title);
}
// prints: "Untitled 0", "My Sheet"
interface AliasOptions
Contains four options, none of which are required:
const myOptions: AliasOptions = {
/**
* One of: "readonly" or "readwrite" (specified by TunnelAccess)
*
* If "readonly" is used then any assignments to the alias result in a
* TypeError.
*
* If a readwrite property is assigned and the destination property is
* unreachable a ReferenceError will be thrown.
*
* N.B. An option may be added in a later version that will permit
* automatic creation of the destination property when it doesn't exist.
*
* Default: "readwrite"
*/
access: "readonly",
/**
* The value of the alias if the destination is null, undefined or
* if one of the intermediate properties is unreachable.
*
* Default: undefined
*/
defaultValue: "my default value",
/**
* If provided maps values to/from the destination property.
*
* See TunnelConverter below.
*/
converter: null,
/**
* Whether the alias should be bound to the destination if the destination
* property is a function.
*
* Default: true
*/
autoBindFunctions: false,
};
tunnel(to, key, { <options> })
Used to create a tunnel on an arbitrary object. Used by alias
to implement
class property aliases.
Options are described by the TunnelOptions interface.
Usage
const destination = { prop: "foobar" };
const proxy = {};
tunnel(proxy, "alias", { destination, destinationKey: "prop" });
console.log(proxy.alias); // prints "foobar"
proxy.alias = "bazquirk";
console.log(destination.prop); // prints "bazquirk"
interface TunnelOptions
Contains all of the options from AliasOptions
plus destination
and
destinationPath
.
const myOptions: TunnelOptions = {
/**
* The destination object that owns the property to tunnel to.
*/
destination: theDestination,
/**
* An array composed of strings, symbols and/or numbers that designates
* the property of `destination` to tunnel to.
*/
destinationPath: ["a", "property", "of", "theDestination"]
};
type TunnelAccess = "readonly" | "readwrite"
Literally one of the strings "readonly"
or "readwrite"
.
interface TunnelConverter
Used to specify two functions that convert between the alias/tunnel and destination values.
You might use this to convert between types or to reformat values.
Usage
const percentConverter = {
// Read as "to tunnel value"
toTunnel(value: number) { return value * 100; },
// Read as "from tunnel value"
fromTunnel(value: number) { return value / 100; }
};
class Progress {
public value: number = 0;
// Then on a class property
@alias(["value"], { converter: percentConverter })
public percent: number;
}
const progress = new Progress();
progress.value = 0.5;
console.log(progress.percent); // prints: 50
progress.percent = 100;
console.log(progress.value); // prints: 1
PropertyTunnel class
This is a lower level class that can be used if you ever need to connect multiple tunnels to the same destination.
It's unlikely you'll ever need to use this. The tunnel()
function uses it
internally and returns the instance it creates.
If you find you need to use it have a look at the src/PropertyTunnel.ts
file,
it's heavily commented.
Notes
I've liberated this library from one of my own proprietary projects. Some of the code that isn't part of the public API has much greater reuse value in the original project.
For the time being I've included some of that code in this library but it will be moved to another open source library I'll be releasing in good time.
It's for this reason that it's particularly important that you don't use anything that isn't explicitly included in the public API as summarised above.