2

Is it possible to do the equivalent provided in this answer, but in Typescript?

Subclassing a Java Builder class

Here is what I have so far for the base class:

export class ProfileBuilder {
    name: string;

    withName(value: string): ProfileBuilder {
        this.name= value;
        return this;
    }

    build(): Profile{
        return new Profile(this);
    }
}

export class Profile {
    private name: string;

    constructor(builder: ProfileBuilder) {
        this.name = builder.Name;
    }
}

And the extended class:

export class CustomerBuilder extends ProfileBuilder  {
    email: string;

    withEmail(value: string): ProfileBuilder {
        this.email = value;
        return this;
    }

    build(): Customer {
        return new Customer(this);
    }
}

export class Customer extends Profile {
    private email: string;

    constructor(builder: CustomerBuilder) {
        super(builder);
        this.email= builder.email;
    }
}

Like the other thread mentions, I won`t be able to build a Customer in this order because of the change of context:

let customer: Customer = new CustomerBuilder().withName('John')
                                              .withEmail('john@email.com')
                                              .build();

I am currently trying to use generics to fix the issue, but I am having trouble when returning the this pointer for my setter methods (type this is not assignable to type T). Any ideas?

user2595996
  • 89
  • 1
  • 6
  • 2
    please take some time to read the https://stackoverflow.com/help/how-to-ask guide. it will help you get answers. :) – toskv May 26 '17 at 14:32
  • 1
    Yep, I'm in the process of editing the question to give a concrete example on what I have so far. – user2595996 May 26 '17 at 14:35

3 Answers3

5

Found a solution! After looking at the different answers on the other thread I mentionned, I ended up creating a base abstract class and builder then extending for each of my class/builder pair:

abstract class BaseProfileBuilder<T extends BaseProfile, B extends BaseProfileBuilder<T, B>> {
    protected object: T;
    protected thisPointer: B;

    protected abstract createObject(): T;

    protected abstract getThisPointer(): B;

    constructor() {
        this.object = this.createObject();
        this.thisPointer = this.getThisPointer();
    }

    withName(value: string): B {
        this.object.name = value;
        return this.thisPointer;
    }

    build(): T {
        return this.object;
    }
}

abstract class BaseProfile {
    name: string;
}

class ProfileBuilder extends BaseProfileBuilder<Profile, ProfileBuilder> {
    createObject(): Profile {
        return new Profile();
    }

    getThisPointer(): ProfileBuilder {
        return this;
    }
}

class Profile extends BaseProfile {
}

class CustomerBuilder extends BaseProfileBuilder<Customer, CustomerBuilder>  {
    createObject(): Customer {
        return new Customer();
    }

    getThisPointer(): CustomerBuilder {
        return this;
    }

    withEmail(value: string): CustomerBuilder {
        this.object.email = value;
        return this;
    }
}

class Customer extends BaseProfile {
    email: string;
}


let customer: Customer = new CustomerBuilder().withName('John')
                                              .withEmail('john@email.com')
                                              .build();

console.log(customer);
user2595996
  • 89
  • 1
  • 6
3

I recently encountered the same requirement and this is my solution, if we create a profile builder class we can extend that from our customer builder and utilise super to call the base builder.

class ProfileBuilder {
    private name: string;

    constructor() {
        this.name = undefined;
    }

    public withName(name: string) {
        this.name = name;
        return this;
    }

    public build() {
        return {
            name: this.name
        }
    }
}

class CustomerBuilder extends ProfileBuilder {
    private email: string;

    constructor() {
        super();

        this.email = undefined;
    }

    public withEmail(email: string) {
        this.email = email;
        return this;
    }

    public build() {
        const base = super.build();
        return {
            ...base,
            email: this.email
        }
    }
}

this will now allow you to create a customer as per your requirement:

const customer = new CustomerBuilder()
    .withName("John")
    .withEmail("john@email.com") 
    .build();
Owen Pattison
  • 314
  • 2
  • 6
1

Set the chaining methods' return type to this

In classes, a special type called this refers dynamically to the type of the current class.

-- Typescript Docs

As a result, the return type of your chained methods will always match the type of builder you instantiated, allowing full access to all the methods available on that builder, whether they are inherited, overridden, or added in the subclass.

// -----------
// Parent Class

class ProfileBuilder {
  name?: string;
  
  // The `this` return type will dynamically match the instance type
  withName(value: string): this {
    this.name = value;
    return this;
  }

  build(): Profile {
    return new Profile(this);
  }
}

class Profile {
  private name: string;

  constructor(builder: ProfileBuilder) {
    this.name = builder.name ?? 'default name';
  }
}


// -----------
// Child class

class CustomerBuilder extends ProfileBuilder {
  email?: string;

  // Return `this` here too to allow further subclassing.
  withEmail(value: string): this {
    this.email = value;
    return this;
  }

  build(): Customer {
    return new Customer(this);
  }
}

class Customer extends Profile {
  private email: string;

  constructor(builder: CustomerBuilder) {
    super(builder);
    this.email = builder.email ?? 'default@email.com';
  }
}



// -----------
// Example Usage

// Notice that the order of the `with` methods no longer matters.

let customer: Customer = new CustomerBuilder()
  .withName('John')
  .withEmail('john@email.com')
  .build();

let customer2: Customer = new CustomerBuilder()
  .withEmail('jane@email.com')
  .withName('Jane')
  .build();

export {};

thehale
  • 913
  • 10
  • 18