Let’s say you want to create a custom class, this class has a constructor parameter. For this example let’s say we want to manage a “person”, more like a “Person”.
type PlainPerson = {
name: string,
age: number,
}
Let’s say from this plain object we want to add some cool getters or methods:
class Person {
constructor (public value: PlainPerson) {}
get name () {
return this.value.name;
}
get age () {
return this.value.age;
}
get isOver20 () {
return this.value.age >= 20
}
}
Let’s now say I want to iterate over many people. I can easily use an Array.
const john = new Person({ name: 'john', age: 30 })
const people = [john]
const filteredPeople = people.filter(person => person.isOver20)
And this works, this is perfectly acceptable. But what if you wanted more?
What if you wanted a collection of Person
to do more then just what a normal array could provide?
What if I wanted to add all ages up for all the “people”?
Then I might consider a People
class as well.
class People {
get totalAge () {
return value.reduce((acc, person) => person.age + acc, 0)
}
}
But now the question is what should the constructor of People
recieve? Should it recieve Person
, or PlainPerson
? Should it recieve an array of these? Can they be spread? Can People
recieve many People
and flatten them down?
I found myself asking these questions and decided to create something that would fix this issue and create a basic reproducable structure for any custom iterable. It kind of works like this:
const [Person, People] = FactoryIterable()
This returns two standard classes that you could use similar to how I described above. A Person
and People
they don’t have any custom methods yet. In order to add them we need to extend these base-classes. So it may be best to rename them. I usually use the prefix Proto
.
const [ProtoPerson, ProtoPeople] = FactoryIterable<PlainPerson>({
multipleConverter: (value: PlainPerson | PlainPerson[] | undefined): PlainPerson[] => {
if (value instanceof Array) {
return value;
} else if (typeof value === "object") {
return [value];
}
throw new Error("Invalid value");
},
})
class Person extends ProtoPerson {
get name () {
return this.value.name;
}
get age () {
return this.value.age;
}
get isOver20 () {
return this.value.age >= 20
}
}
class People extends ProtoPeople<typeof Person, typeof People> {
arrayMethods = this.generate(Person, People);
get totalAge () {
return value.reduce((acc, person) => person.age + acc, 0)
}
}
The coolest thing about how these work is the input resolving. A Person
can take PlainPerson
and
const plainJohn = { name: "John", age: 20 }
const john = new Person(plainJohn)
const john2 = new Person(john)
assertEquals(john.value, plainJohn)
assertEquals(john2.value, plainJohn)
const a = new People(plainJohn)
const b = new People(john)
const c = new People(a)
const d = new People(b)
const e = new People([plainJohn])
const f = new People([john])
const g = new People([a])
assertEquals(a.value, [plainJohn])
assertEquals(b.value, [plainJohn])
assertEquals(c.value, [plainJohn])
assertEquals(d.value, [plainJohn])
assertEquals(e.value, [plainJohn])
assertEquals(f.value, [plainJohn])
assertEquals(g.value, [plainJohn])
That’s a pretty bare-bones overview of how you’d use it. There are a could of extra details when it comes to input types and even circular input types, as well as adding generics. I have a couple examples of FactoryIterable
in the wild as well as the source code.
Here’s a main working test that demos this Person / People class https://github.com/reggi/tree_lint2/blob/main/factory_iterable/test.ts
Here’s the source https://github.com/reggi/tree_lint2/blob/main/factory_iterable/mod.ts
Here are a couple other examples:
Node: https://github.com/reggi/tree_lint2/blob/main/graph/node/mod.ts
EdgeNode https://github.com/reggi/tree_lint2/blob/main/graph/edge_node/mod.ts
Edge https://github.com/reggi/tree_lint2/blob/main/graph/edge/mod.ts