A custom "iterable" class

July 8, 2023 (Syndicated From dev.to)

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