Nested class or inner class in Typescript

JavaScript/TypeScript

Is it possible to define nested/inner classes in typescript? Yes, it is. Go to chapter 3 or 4 if you want to know only nested/inner classes.

Clone the repository if you want to try it on your own.

https://github.com/yuto-yuto/BlogPost/tree/master/src/simple-code/nested-class
Sponsored links

Problem to solve

I use Node-RED in my project and write lots of flow tests. There are multiple flow files and they have the same output node for different inputs. It means that I have to prepare the same expected data for the different flows. The expected data looks like the following.

const expected = {
    payload: {
        timestamp: "2021-06-20T10:15:18.123Z",
        key: "status",
        value: "running",
    }
};

When the possible value is 1 – 5 I had to write all of them in the different tests. However, sometimes I forgot to write one of those tests. I didn’t want to repeat the mistake, so I decided to create common classes used for multiple flow tests. I less often forgot to write necessary tests in this way. The classes have functions that return possible values. Following is an example.

export class MachineStatus {
    public static timestamp= "2021-06-20T10:15:18.123Z";
    public static running() {
        return {
            payload: 1,
        };
    }
    public static stop() {
        return {
            payload: 2,
        };
    }
}

export class OutputStatus {
    private key = "status";
    public static running(timestamp: string) {
        return {
            payload: {
                timestamp,
                key: this.key,
                value: "running",
            }
        };
    }
    public static stop(timestamp: string) {
        return {
            payload: {
                timestamp,
                key: this.key,
                value: "stop",
            }
        };
    }
}

// Test file
const input = MachineStatus.running();
const expected = OutputStatus.running(MachineStatus.timestamp);
runTest(input, expected);

One test file uses MachineStatus but another use AudioStatus for example but both of them use OutputStatus. I don’t have to change many files in this way even if I need to change the possible value from running to Running.

I created a test data class for each input but those classes look similar or the same. If its value is boolean the class has true and false. If its value is a number, the class has create(value: number) or return100() for example. There are many classes that are actually the same structure. This is the problem I want to solve. I want to remove the duplication.

Sponsored links

Using extends keyword like type Alias

type alias is not available for class but we can use extends keyword instead.

Boolean base

Let’s see the actual code. Firstly, Boolean.

abstract class BooleanBase {
    public static timestamp = "2021-06-20T10:15:18.123Z";
    public static get true() {
        return {
            timestamp: BooleanBase.timestamp,
            key: this.name,
            payload: true,
        };
    }
    public static get false() {
        return {
            timestamp: BooleanBase.timestamp,
            key: this.name,
            payload: false,
        };
    }
}
console.log("---BooleanBase---")
console.log(BooleanBase.true);
console.log(BooleanBase.false);

// ---BooleanBase---
// {
//   timestamp: '2021-06-20T10:15:18.123Z',
//   key: 'BooleanBase',
//   payload: true
// }
// {
//   timestamp: '2021-06-20T10:15:18.123Z',
//   key: 'BooleanBase',
//   payload: false
// }

This is the base class used by all boolean statuses. You can create a new item class by extends.

class Light extends BooleanBase { }
console.log("---Light---")
console.log(Light.true);
console.log(Light.false);

// ---Light---
// { timestamp: '2021-06-20T10:15:18.123Z', key: 'Light', payload: true }
// { timestamp: '2021-06-20T10:15:18.123Z', key: 'Light', payload: false }

In this way, we can easily create new classes as many as we want. I’m wondering why the output format is different…

Number base

abstract class NumberBase {
    public static timestamp = "2021-06-22T22:22:22.222Z";
    public static create(data: number) {
        return {
            timestamp: NumberBase.timestamp,
            key: this.name,
            payload: data,
        }
    }
}
class Speed extends NumberBase { }
console.log("---NumberBase---")
console.log(NumberBase.create(12));
// ---NumberBase---
// {
//   timestamp: '2021-06-22T22:22:22.222Z',
//   key: 'NumberBase',
//   payload: 12
// }
console.log("---Speed---")
console.log(Speed.create(120));
// ---Speed---
// { timestamp: '2021-06-22T22:22:22.222Z', key: 'Speed', payload: 120 }

Nested class or inner class

We need to put more than 2 items together for readability or maintainability. A nested class or inner class can use for it. However, there are other ways to do the same thing. It’s a function that returns an object that contains desired functions. Then, the class can have a property that has the same functions.

const globalTestTimestamp = "2021-06-20T11:11:11.111Z";
function createPayloadFuncs(key: string) {
    return {
        true: () => {
            return {
                timestamp: globalTestTimestamp,
                key,
                payload: true,
            };
        },
        false: () => {
            return {
                timestamp: globalTestTimestamp,
                key,
                payload: false,
            };
        },
    };
}
class Car {
    public static FrontLight = class extends BooleanBase { };
    public static BackLight = createPayloadFuncs("BackLight");
    public AveSpeed = class extends NumberBase { };
    public CurrentSpeed = class extends NumberBase { };
}

console.log("---Car---")
console.log(Car.BackLight.false());
console.log(Car.FrontLight.true);
// ---Car---
// {
//   timestamp: '2021-06-20T11:11:11.111Z',
//   key: 'BackLight',
//   payload: false
// }
// {
//   timestamp: '2021-06-20T10:15:18.123Z',
//   key: 'BooleanBase',
//   payload: true
// }
const car1 = new Car();
console.log(car1.AveSpeed.create(12));
console.log(car1.CurrentSpeed.create(80));
// {
//   timestamp: '2021-06-22T22:22:22.222Z',
//   key: 'NumberBase',
//   payload: 12
// }
// {
//   timestamp: '2021-06-22T22:22:22.222Z',
//   key: 'NumberBase',
//   payload: 80
// }

car1.extension.honk();
const car2 = Car.Factory.create();
car2.extension.honk();

Car.FrontLight.true can be called like this while Car.BackLight.false() needs () because createPayloadFuncs returns object where we can’t set the property as a getter.
Look at the key output. this.name returns the name of the base class even though it extends it.

console.log(Car.FrontLight.true);
// {
//   timestamp: '2021-06-20T10:15:18.123Z',
//   key: 'BooleanBase',
//   payload: true
// }

I don’t know if it’s available to solve this with the current typescript version. The version that I used was 3.9.7.

Define Factory class as nested/inner class

Let’s see another example that might be useful to define nested/inner classes. It’s a factory class that wants to refer to private variables in the outer class.

class Car {
    private static count = 0;
    private constructor(private carNumber: number) { }

    public extension = {
        honk: () => console.log(`beeee: ${this.carNumber}`),
    };
    public static Factory = class {
        public static create(): Car {
            Car.count++;
            return new Car(Car.count);
        }
    }
}
const car1 = Car.Factory.create();
car1.extension.honk();
// beeee: 1
const car2 = Car.Factory.create();
car2.extension.honk();
// beeee: 2

It works as expected and its name is clear to show that Car.Factory.create() creates a new instance. However, this is not the best way in my opinion. I think it’s enough to define static factory function in Car class. Please leave your comment if you know better use cases.

Comments

Copied title and URL