Quick Post: Filter Records in TypeScript using Builder Pattern

Today, we revisit the builder pattern and instead of using C#, we're building it in TypeScript

Written by Jonathan "JD" Danylko • Last Updated: • Develop •

Construction Workers...working

For every language, there is always a design pattern whether it's JavaScript, .NET, Ruby, or even COBOL (Just kidding, there aren't any design patterns for COBOL). Each design pattern solves a particular programming problem.

The good news is once you understand design patterns in one language, you'll be able to apply the same concept in another language making it easier for others to understand. 

One of my favorite design patterns is the builder, or fluent, pattern. I've used it to build complex objects at a granular level and custom filters for a list of records or products. You know you've built a great builder pattern when you can read it like a sentence.

For example, think about a product website where the user wants a product list based on user criteria similar to Newegg or Amazon. How would you write code for it?

What's the alternative? You could have a large number of if..then statements, but then that adds to your cyclomatic complexity making code harder to understand. With the builder approach, you can apply complex queries to a simple list of records.

With our previous post using the builder pattern in C# and the ability to use TypeScript, we can mimic the builder pattern for filtering data.

In this quick post, I'll show you how to convert the builder pattern from C# into TypeScript.

Our Product Inventory (for Black Friday?)

Let's get our products together for the upcoming holiday season.

class Product {
    constructor(
        public id: number,
        public title: string,
        public available: Date,
        public rating: number,
        public includeComments: boolean
    ) { }
}

const
 records = [     new Product(1, 'Product 1', new Date('10/01/2020'), 4, false),     new Product(2, 'Product 2', new Date('08/15/2020'), 1, true),     new Product(3, 'Product 3', new Date('09/18/2020'), 3, true),     new Product(4, 'Product 4', new Date('09/15/2020'), 5, false),     new Product(5, 'Product 5', new Date('12/01/2020'), 4, true) ];

We want to see when it's available, the rating, and whether we want to include comments.

Once we have our data, we can create our FilterBuilder.

Building the Builder

Our FilterBuilder takes in a list of clean products and we need a way to return just the filtered records. 

class FilterBuilder {

    private _filtered: Product[];
    // Defaults     _avilableDate: Date = new Date();     _rating: number = 0;     _includeComments: boolean = false;
    constructor(private _records: Product[]) {         this._filtered = _records;     }
    setAvailableDate(availableDate: string) {         this._avilableDate = new Date(availableDate);         this._filtered = this._filtered.filter(             (item, index) =>             item.available <= new Date());
        return this     }
    setRating(value: number) {         this._rating = value;         this._filtered = this._filtered.filter(             (item, index) => item.rating >= value);         return this;     }
    includeComments(value: boolean) {         this._includeComments = value;         this._filtered = this._filtered.filter(             (item, index) => item.includeComments === value);         return this;     }
    build() {         return this._filtered;     } }

Let's breakdown the setRating() method.

setRating(value: number) {
    this._rating = value;
    this._filtered = this._filtered.filter(
        (item, index) => item.rating >= value);
    return this;
}

The first line is optional, but I like to have an object where I can save off the filter if I want to. Whatever you assign to those values as you're building your query, you'll be able to identify the parameters if something fails.

Next, we work with the filtered list of records, NOT the private _records field. We only want ratings greater than the user's selection (only rating X or higher) using the filter method.

Finally, we return "this." The "this" refers to the instance of the object. This allows you to chain methods together to get your fluent syntax.

One caveat to this: In my builder pattern in C# post, I placed all of the if..statements inside the build() method. One reader mentioned SonarQube was screaming because of all of the if...statements checking for valid filter values in the build() method. It makes more sense to place the if..statements in each filter method like:

setRating(value: number) {
    if (value > 0 && value !== this._rating) {
        this._rating = value;
        this._filtered = this._filtered.filter(
            (item, index) => item.rating >= value);
    }
    return this;
}

It isolates the "business logic" into one method instead of tracking it down across the class. If our value wasn't valid, it won't apply the filter. 

How do we use it?

Now the fun part.

const results = new FilterBuilder(records)
    .setAvailableDate('10/31/2020')
    .includeComments(true)
    .setRating(4)
    .build();

console
.log(results);

Our FilterBuilder takes in a list of records (from above) and we set our filtering conditions through the methods with the last call returning our filtered list of records to display to the user.

Why Use The Builder Pattern?

There are three reasons to use this pattern.

1. Separation of Concerns

When you have a clean list of records and you want to apply a filter to them, it's more maintainable to abstract a filtering class to manage the user's view of the products.

2. Extensibility

If the user wants to refine their search based on more fields, this design pattern makes it easy to extend the FilterBuilder by adding more methods for the user to fine-tune their results.

3. Testing

Testing a class that's self-contained is extremely simple.

Our FilterBuilder takes in a list of Products which we can easily mock for testing purposes and assert the number of records returned.

What's the final source code look like?

With everything we discussed in this post, the final code looks like this:

class Product {
    constructor(
        public id: number,
        public title: string,
        public available: Date,
        public rating: number,
        public includeComments: boolean
    ) { }
}

const
 records = [     new Product(1, 'Product 1', new Date('10/01/2020'), 4, false),     new Product(2, 'Product 2', new Date('08/15/2020'), 1, true),     new Product(3, 'Product 3', new Date('09/18/2020'), 3, true),     new Product(4, 'Product 4', new Date('09/15/2020'), 5, false),     new Product(5, 'Product 5', new Date('12/01/2020'), 4, true) ];
class
 FilterBuilder {
    private _filtered: Product[];
    // Defaults     _avilableDate: Date = new Date();     _rating: number = 0;     _includeComments: boolean = false;
    constructor(private _records: Product[]) {         this._filtered = _records;     }
    setAvailableDate(availableDate: string) {         this._avilableDate = new Date(availableDate);         this._filtered = this._filtered.filter(             (item, index) =>             item.available <= new Date());
        return this     }
    setRating(value: number) {         this._rating = value;         this._filtered = this._filtered.filter(             (item, index) => item.rating >= value);         return this;     }
    includeComments(value: boolean) {         this._includeComments = value;         this._filtered = this._filtered.filter(             (item, index) => item.includeComments === value);         return this;     }
    build() {         return this._filtered;     } }
const
 results = new FilterBuilder(records)     .setAvailableDate('10/31/2020')     .includeComments(true)     .setRating(4)     .build();
console
.log(results);

When I wrote this, I used the TypeScript Playground, so copy-and-paste this into the playground, run it, and experiment with the methods to change the results.

Conclusion

The best part of this design pattern is we can use it in various object-oriented languages where you give the user control over how they want to display their records.

Another way to use the builder pattern is to create a fascade-ish pattern to make it easy to use a complex underlying syntax. A fascade design pattern takes a simplified abstraction (could be the builder pattern) and lays that on top of a complex subsystem making it easier to use.

While our FilterBuilder here is truly simplified, you can create easier methods to mask the complexity of other systems using "programmer's grammar."

Have you used the Builder pattern before? What did you build with it? Post your comments below and let's discuss.

Did you like this content? Show your support by buying me a coffee.

Buy me a coffee  Buy me a coffee
Picture of Jonathan "JD" Danylko

Jonathan Danylko is a web architect and entrepreneur who's been programming for over 25 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Principal Software Engineer Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus