Constructor Chain
"Chainables" enable chain-style extension of class statics in TypeScript.
Compared with class extends
syntax, chainables are a lighter mechanism for composition of metadata within constructors. The goal is to allow TypeScript library developers to treat constructors as more of a first-class data type. This pattern can serve libraries which aim to track state in user-provided constructors.
Getting Started
Installation
Deno users can reference the GitHub source directly.
import_map.json
Node users can install with npm.
npm install constructor-chain
constructor-chain
is packaged in both ESM & CJS formats, alongside its type definitions.
Basic Usage
; ; A.a; // type `"hello"`new A; // instance of `A` ; B.a; // type `"hello"`B.b; // type `"chainable"`new B; // instance of `B` (subtype of `A`) ; C.staticsToString; // `Hello chainables!`new C; // instance of `C` (subtype of `B`)
Usage Continued
Let's say we're building a validation library which accepts a custom String
constructor. Our validation library expects for the constructor to contain metadata about its constraints in the form of static props. This validation metadata could include minLength
, maxLength
, forbiddenChars
, ... the list goes on.
First, we'll define our base constructor, which extends String
.
Under normal (non-chainable) circumstances, we would extend from OurString
to encode validation metadata onto our constructors.
BAD
If we want to then recombine these static fields in new constructors, this becomes tedious. We end up writing a lot more than the chainable equivalent:
GOOD
; ; ;;;
These all remain valid constructors.
new MinLengthString"min length string"; // type `string`new MaxLengthString"max length string"; // type `string`new ForbiddenCharsString"forbidden chars string"; // type `string`
Let's now abstract over chainables with a few helpers.
; // the constructor you wish make chainable; // your metadata factories ; ;;;
We can pass metadata down the chain as well.
;;
The chainable constructors can be used to produce new constructors. Statics are represented as an intersection of the initial constructor and a recursive object type, generic over the chain; this representation allows us to overcome the thoughtful, yet sometimes annoying limitations of static properties in TypeScript.
For instance, one cannot write the following without producing TS error 1166 (A computed property name in a class property declaration must refer to an expression whose type is a literal type or a 'unique symbol'
).
BAD
; ;
Let's see how chainables make this possible, without producing a compile error.
GOOD
; ; ;
Caveat
A.a; // type `any`new A; B.b; // type `any`new B; C.c; // type `any`new C;
Nooks & Crannies
this
Context
When providing statics to the next
method, the this
context (the aggregate of the chain's contexts) is available.
; ; B.printFqn; // logs out "j3$1Ks"
Supplying a Custom Next Key
To supplement an alternative method name for next
, pass the desired name as the second argument of Chainable
.
; // `proceed` instead of `next`;
Note
It's my humble belief that its alright if it's unclear how a piece of technology will be used. This library was built primarily out of enjoyment of the process, and its use cases are a secondary consideration. I do believe the Chainable
type has its place, but who knows? TypeScript's statics are unusual beasts: chainables allow us to represent them without actually making use of the type system's representation of statics. This enables us to tackle more composition patterns without those gruesome red squiglies (in addition to reducing inheritance boilerplate). If you have a use case in mind, or feedback more generally, please do reach out––I'd love to hear from you!
License
This library is licensed under the Apache 2.0 License.