firm-path
v0.2.1
Published
Typed access to deep object fields via paths built using TypeScript
Downloads
6
Maintainers
Readme
firm-path
Typed access to deep object fields via paths, built using TypeScript
This package provides a way to create strongly-typed access to object fields. Paths can be created directly, or can be created from templates using specific values (such as an array index) when required.
Path roots also cache/memoize the paths and templates that are created under them, so path instances are singletons that can be compared using ===
or used as keys elsewhere if desired.
To try firm-path
in an interactive example, you can use this stackblitz that contains the example below.
Installation
NPM: npm install firm-path --save
Yarn: yarn install firm-path
Example Usage
To use firm-path
, first import the getRootPath
function.
import { getRootPath } from 'firm-path';
In order to strongly type access to objects, first create or derive a type that describes their shape.
interface IAddress {
lines: string[];
postcode: number;
}
Then obtain an instance to the root path for the type. The root path is used as a starting point to build sub-paths.
const rootPath = getRootPath<IAddress>();
Paths are built using an arrow function, so that typescript can provide intellisense for the available fields. Note that any functions defined in the type are explicitly excluded.
// An example of a simple field path
const postcodePath = rootPath.getSubPath(a => a.postcode);
// Paths can point to primitives or more complex objects
const linesPath = rootPath.getSubPath(a => a.lines);
// Paths can build sub-paths, and indexing can be used
const firstLinePath = linesPath.getSubPath(ls => ls[0]);
"Templates" can be derived to defer having to specify some parts of a path until later.
const lineTemplate = linesPath.getDynamicChild();
Create or derive an instance of the object to be interrogated. A path can be used to get the object value.
const address = {
lines: ['101 Somewhere St', 'Smalltown'],
postcode: 12345,
};
// postcode value is number 123456 (typed as number | undefined)
const postcode = postcodePath.getValue(address);
// firstLine value is string '101 Somewhere St' (typed as string | undefined)
const firstLine = firstLinePath.getValue(address);
// The argument to getPath is a typed tuple
// secondLine value is string 'Smalltown'
const secondLinePath = lineTemplate.getPath([1]);
const secondLine = secondLinePath.getValue(address);
// thirdLine value is undefined
const thirdLinePath = lineTemplate.getPath([2]);
const thirdLine = secondLinePath.getValue(address);
Paths can also be used to set or remove the particular value.
// address.postcode is now 98765
postcodePath.setValue(address, 98765);
// address.lines is now ['101 Somewhere St']
secondLinePath.removeValue(address);
The key values for a Path can be retrieved if desired.
// secondLinePathParts value is ["lines", 1]
const secondLinePathParts = secondLinePath.parts;
Features
- Access to the object values is strongly typed by TypeScript.
- Paths and Templates are immutable.
- Paths and Template instances are cached within the Root Path instance, and thus can be compared using
===
. - Paths and Templates can contain Symbols
- Templates can contain multiple dynamic parts (eg: arrays of arrays)
- Updating an object (via
setValue
orremoveValue
) mutates the provided object. If you would prefer not to mutate the original instance, consider wrapping the call usingimmer
.
Why would I use this?
Until JavaScript/TypeScript gets Optional Chaining, deep object getting and setting can be performed without having to do falsey checks at each level.
Using Paths would usually be useful when wanting to genericise some behaviour. For example, a function could retrieve, check, then update a value in a supplied object, with a Path to define which field to operate on. For example:
interface IItem { value: number; subItem: { subValue: number; }; } function addOne(obj: IItem, path: IPath<IItem, number>) { const value = path.getValue(obj); const newValue = value === undefined ? 0 : value + 1; path.setValue(obj, newValue); } const root = getRootPath<IItem>(); const valuePath = root.getSubPath(i => i.value); const subValuePath = root.getSubPath(i => i.subItem.subValue); const testObj = { value: 1, subItem: { subValue: 10 } }; addOne(testObj, valuePath); addOne(testObj, subValuePath);