gypsydave5

The blog of David Wickes, software developer

this is my type

Ah man, the shit you can do in TypeScript with this as a type. Let me show you:

this as a return type

Who hasn’t written something using the builder pattern? Ok, so probably a few of you. The idea is that you construct an object by adding to it incrementally with method calls, then “finalize” it using another method. Like this:

const dog = new DogBuilder()
	.withName('Erik')
	.withWeightKg(15)
	.withCoat(Colors.OrangeRoan)
	.withBreed(Breeds.CockerSpaniel)
	.build()

There are benefits, there are downsides, blah blah blah. You might prefer to do the same thing using a configuration object with defaults. Go read and have fun.

Things get interesting on the inside of our DogBuilder:

class DogBuilder {
    private name = 'Rover';
    private weight = 22;
    private coat = Color.Black;
    private breed = Breeds.Labrador
    
    
    public withName(name :string) {
        this.name = name
        return this
    }
    
    public withWeightKg(weight: number) {
        this.weight = weight
        return this
    }
    
    public withCoat(coat: Colors) {
        this.coat = coat
        return this
    }
    
    public withBreed(breed: Breed) {
        this.breed = breed
        return this
    }
    
    public build(): Dog {
        return new Dog(this.name, this.weight, this.coat, this.breed)
    }
}

All good I hope. So what’s the return type for each of our methods? Well, pretty obviously it’s a DogBuilder. We get a builder back each time until we call build() - that’s how the builder works. We can be explicit about this:

class DogBuilder {
    private name = 'Rover';
    private weight = 22;
    private coat = Color.Black;
    private breed = Breeds.Labrador
    
    
    public withName(name :string): DogBuilder {
        this.name = name
        return this
    }
    
    public withWeightKg(weight: number): DogBuilder {
        this.weight = weight
        return this
    }
    
    public withCoat(coat: Colors): DogBuilder {
        this.coat = coat
        return this
    }
    
    public withBreed(breed: Breed): DogBuilder {
        this.breed = breed
        return this
    }
    
    public build(): Dog {
        return new Dog(this.name, this.weight, this.coat, this.breed)
    }
}[ cc[m
m[mm]]]

OK - great. Now make me a PoodleBuilder for my new Poodle class. Poodles come in four sizes: standard, medium, miniature and toy, so add let’s have a nice enum for that too. Go on, get on with it. Oh alright, I’ll do it.,,,[[]]

Now we could implement this in a few ways, but let’s take a look at using inheritance to reduce the code duplication (yes, we could use composition, no we’re not going to).

class PoodleBuilder extends DogBuilder {
    private name = 'PoPo';
    private weight = 22;
    private coat = Color.Black;
    private breed = Breeds.Poodle
    private size = Size.medium
    
    public withSize(size: Size): PoodleBuilder {
        this.size = size
        return this
    }
    
    public build(): Poodle {
        return new Poodle(this.name, this.weight, this.coat, this.breed, this.size)
    }
}

Well now things are going to get interesting.

const poodle: Poodle = new PoodleBuilder().withName('Fluffy').withWeightKg(1).withCoat(Colors.White).withSize(Size.toy).build()

This will blow up spectacularly when we run the type checker.

TS2339: Property 'withSize' does not exist on type 'DogBuilder'

and check out:

const anotherPoodle: Poodle = new PoodleBuilder().withName('Fluffy').withWeightKg(1).withCoat(Color.White).build()
TS2741: Property 'size' is missing in type 'Dog' but required in type 'Poodle'.

Yeah, that ain’t no Poodle we got there, that’s a Dog. And that ain’t no PoodleBuilder we’re getting after withName() in the first example, that’s a DogBuilder. Or at least, that’s what the type system thinks. How can we convince it that it’s really a PoodleBuilder?

We tell it that the return type of the methods in the DogBuilder is this. Yes - this! this is a type, and it refers to the type of… well, this. At the risk of showing my age, check it out:

class DogBuilder {
    private name = 'Rover';
    private weight = 22;
    private coat = Color.Black;
    private breed = Breeds.Labrador
    
    
    public withName(name :string): this {
        this.name = name
        return this
    }
    
    public withWeightKg(weight: number): this {
        this.weight = weight
        return this
    }
    
    public withCoat(coat: Colors): this {
        this.coat = coat
        return this
    }
    
    public withBreed(breed: Breed): this {
        this.breed = breed
        return this
    }
    
    public build(): Dog {
        return new Dog(this.name, this.weight, this.coat, this.breed)
    }
}

and all those type issues just melt away!

The this type will be inferred - you don’t always need to declare it as the return type of a method. But writing it out explicitly can help improve our understanding.

So - what else can we do with this?

this as the receiver type

Who hasn’t done this:

class Dog {
    constructor(public readonly name: string, readonly breed: Breed) {
    }

    sayHello(): string {
        console.log(`Hello, I'm a ${this.breed} called ${this.name}`)
    }
}

const rover = new Dog('Rover', Breed.Poodle)
setTimeout(() => rover.sayHello(), 10)
// => Hello, I'm a Poodle called Rover

oh we can just remove that outer function through eta reduction and it’ll do the same thing. Functional programming for the win:

setTimeout(rover.sayHello, 10)
// => Hello, I'm a undefined called undefined

Curse you this! Yeah, this happens to every JavaScript and TypeScript developer - we’ve lost our binding to the receiver when we passed the function uncalled into setTimeout. So now this is undefined.

How can we fix this? Well, that’s also one of the first things you will work out: you use bind():

setTimeout(rover.sayHello.bind(rover))
// => Hello, I'm a Poodle called Rover

Beautiful, all fixed, well done everyone. But how can we avoid having the problem in the first place? How can we make sure that the function always has the correct this context when it’s executing?

Well, it’s not going to be too much of a surprise given the title of this article, so here’s what it looks like anyway:

class Dog {
    constructor(public readonly name: string, readonly breed: Breed) {
    }

    sayHello(this: Dog): string {
        console.log(`Hello, I'm a ${this.breed} called ${this.name}`)
    }
}

Yeah, so there you go. The this pseudo parameter that makes you feel like you’re writing Python, or Go, or Rust, or some other superior language. Yay.