npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nprindle/bob

v1.0.1

Published

Super typesafe, boilerplate-free builders for TypeScript

Downloads

4

Readme

bob

Build Status

Super typesafe, boilerplate-free builders for TypeScript.

Motivation

Builders are a very useful for declaratively constructing complex objects.

type User = {
    email: string;
    id: number;
};

class UserBuilder {
    private obj: Partial<User> = {};

    setEmail(email: string): UserBuilder {
        this.obj.email = email;
        return this;
    }

    setId(id: number): UserBuilder {
        this.obj.id = id;
        return this;
    }

    build(): User {
        return this.obj as User;
    };
}

const user = new UserBuilder()
    .setEmail("[email protected]")
    .setId(999)
    .build();

Unfortunately, there are a few problems with the above. Firstly, it's not very typesafe; if we don't provide all of the required fields, then we don't find out until runtime, due to the unsafe as User type assertion:

const user = new UserBuilder()
    .setEmail("[email protected]")
    .build();
console.log(user.id); // undefined!

Fortunately, there are many ways that we can take advantage of TypeScript's structural type system to increase our type safety:

type User = {
    email: string;
    id: number;
};

class UserBuilder implements Partial<User> {
    email?: string;
    id?: number;

    setEmail(email: string): this & Pick<User, "email"> {
        return Object.assign(this, { email });
    }

    setId(id: number): this & Pick<User, "id"> {
        return Object.assign(this, { id });
    }

    build(this: User): User {
        return {
            email: this.email,
            id: this.id
        };
    };
}

// Fails to compile, missing 'id'
const user = new UserBuilder()
    .setEmail("[email protected]")
    .build();

Much better! However, this is a lot of boilerplate; it would be a pain to write a builder for all of our types. Libraries such as builder-pattern take advantage of Proxy to automatically create a builder for you, using the proxy's handler to set the necessary fields. This would let you write something like the following (using bob's syntax):

const user = builder<User>()
    .email("[email protected]")
    .id(999)
    .build();

bob combines these two approaches, allowing you to make flexible and typesafe builders without all of the regular boilerplate.

Usage

To automatically make a builder for your type, use builder:

type User = {
    email: string;
    id: number;
    age?: number;
};

const user1 = builder<User>()
    .email("[email protected]")
    .id(999)
    .age(20)
    .build();

// Optional fields work as expected
const user2 = builder<User>()
    .email("[email protected]")
    .id(999)
    .build();

// Compile error! Builder missing required field 'email'
const user2 = builder<User>()
    .id(999)
    .build();

Of course, you can make a function to generate the builders of the desired type for you:

function userBuilder(): Builder<User> = {
    return builder<User>();
}

const user = userBuilder()
    .email("[email protected]")
    .id(999)
    .build();

To make a builder with default values, use builderDef:

function userBuilder(): Builder<User, { id: number }> {
    return builder({ id: 0 });
}

const user1 = userBuilder()
    .email("[email protected]")
    .build();

// Can override defaults if necessary
const user2 = userBuilder()
    .email("[email protected]")
    .id(999)
    .build();

To make an instance out of the built fields, builder and builderDef can optionally accept a function as a second argument, which lets you specify how to construct an instance out of the fields you've built:

class User {
    constructor(
        public readonly email: string,
        public readonly id: number,
    ) {}

    // The first type parameter is the result of .build()
    // The second type parameter is the fields that the builder should have
    static builder(): Builder<User, { email: string; id: number; }> {
        return builder(({ email, id }) => new User(email, id));
    }
}

const user: User = User.builder()
    .email("[email protected]")
    .id(999)
    .build();