static-tree
v1.3.1
Published
Zero dependency builder for strongly typed static tree
Downloads
12
Maintainers
Readme
static-tree
This package is part of the static-tree
monorepo
Table of Contents
Installation
npm install static-tree
yarn add static-tree
pnpm add static-tree
Quick Start
Use the idiomatic
tBuild
to create a static tree. See tBuild for examples and options.import { tBuild } from 'static-tree'; const { node: api } = tBuild('api', { // you can rename node here to whatever pathResolver: () => 'https://api.domain.example', build: (builder) => builder .addChild('auth', { build: (builder) => builder .addChild('logout') .addChild('oauth', { build: (builder) => builder.addChild('google').addChild('discord'), //... }) }) .addChild('camelCaseKey', { pathResolver: () => 'camel-case-key', }) .addChild(':organization', { // notice some dynamic path here pathResolver: (_node, arg) => arg, build: (builder) => builder.addChild(':user', { pathResolver: (_node, arg) => arg, }), }), });
The declaration above will produce this tree structure:
api (resolve statically to 'https://api.domain.example' at runtime) | |-- auth . |-- logout . |-- oauth . | . |-- google . |-- discord |-- camelCaseKey (resolved statically to 'camel-case-key' at runtime) |-- :organization (resolved dynamically to the given argument at runtime) | |-- :user (resolved dynamically to the given argument at runtime)
Access type-safe nested children
root.auth.oauth.google.$.path(); // -> 'https://api.domain.example/auth/oauth/google' // note that ":" here has no special effect // just for easier recognition as dynamic (think backend router system) root[':organization'][':user'].$.path({ args: { ':organization': 'test-org', ':user': 'test-user', } }); // -> 'https://api.domain.example/test-org/test-user' root.auth.logout.$.depth(); // -> 3 root.auth.oauth.discord.$.root() // -> point back to root node
the
$
getter returns the TNodePublicApi collection of methods.Access type-safe
data
root.$.data(); // -> { some: 'data' } root.nestedChild.$.data().nestedChildData; // -> 101
Documentation & Terminologies
This repo includes a full api extracted documentation generated by @microsoft/api-extractor & @microsoft/api-documenter. Please refer to said docs for examples and details.
| Terminology | Description | | --- | --- | | static tree | a tree whose nodes are declared at build time and not likely to change at runtime | | TNode | a node of the static tree with optional inner TNodeData, optional parent, and zero or more children | | ExtendedTNode | a TNode with children inline as properties for better DX | | ExtendedTNodeBuilder | type-safe builder for ExtendedTNode | | tBuild | functional wrapper for ExtendedTNodeBuilder |
Original Use Case
This package was derived from the solution to a specific problem I encountered frequently. Consider having this "config" object:
const AppConfig = {
urls: {
web: 'https://domain.example',
api: {
index: 'https://api.domain.example',
auth: {
index: '/auth',
logout: '/logout',
oauth: {
index: '/oauth',
google: '/google',
// ...
},
},
},
},
};
To get a full api url of google oauth, we have to do quite a lot:
const { api: { auth, index } } = AppConfig.urls;
const path = index + auth.index + auth.index.oauth.index + auth.oauth.google;
Already there are some problems:
Verbosity: lots of reference to get to something, more typing equals more typos equals less productive time.
The inconsistency of the config structure: some path will require an object
index
, some path is just a string. We could refactor to something more predictable, although i think we can agree that this would quickly get out of hand and is very disorienting to look at:const AppConfig = { urls: { web: { base: 'https://domain.example', paths: {}, }, api: { base: 'https://api.domain.example', paths: { auth: { base: '/auth', paths: { logout: { base: '/logout', }, // ... }, }, }, }, }, };
Introducing static-tree
, arguably a better alternative to the above.
import { tBuild } from 'static-tree';
const { node: api } = tBuild('api', {
pathResolver: () => 'https://api.domain.example',
build: (builder) => builder
.addChild('auth', {
build: (builder) => builder
.addChild('logout')
.addChild('oauth', {
build: (builder) => builder
.addChild('google')
.addChild('discord'),
//...
}),
}),
});
You might say, why the ugly builder pattern? Because I have not figured out any other pattern that allows the same level of type-safety. It seems like a lot for what would be an object declaration, but consider what we can do now:
api.auth.oauth.google.$.path(); // -> 'https://api.domain.example/auth/oauth/google'
Even more cool things (and perhaps more in the future if we need to extend the api):
api.auth.oauth.google.$.path({ depth: 2 }); // -> 'oauth/google'
api.auth.oauth.google.$.path({ depth: -2 }); // -> 'https://api.domain.example/auth'