Best way to implement utility function Top-level vs Static vs Namespace

JavaScript/TypeScript

There are several ways to offer utility function which is used by variety classes. Have you ever considered which way is the best to offer the functionality? Let’s have a look on it in this post.

Sponsored links

Top Level function

Top level function is the first candidate to use because it is the simplest one. We need to write import section to use the function.

// Util.ts
export function return3() {
    return 3;
}

// app.ts
import { return3 } from "./Util";
console.log(return3());
// 3

This is very simple and easy to understand. I remember that I couldn’t replace a top level function with stub before but it is possible now. Maybe it depends on the versions of typescript and sinon. Anyway, if we want to stub the function we need to import the function with asterisk in test code. Then, the target function can be replaced via the object util in this case below.

import * as util from "./Util";
describe("Example", () => {
    it("should not be able to stub", () => {
        const obj = new Example();
        sinon.stub(util, "return3").returns(99);
        const result = obj.return5();
        expect(result).to.equal(101);
    });
});
Sponsored links

Static function in a class

Next option is to wrap the function by a class and make it public static function.

// SampleClass.ts
export class SampleClass {
    public static return2() {
        return 2;
    }
}

// app.ts
import { SampleClass } from "./SampleClass";
console.log(SampleClass.return2());
// 2

But if we import the whole file a caller needs to call the function with awkward manner.

import * as SampleClass from "./SampleClass";
// SampleClass.SampleClass ...
console.log(SampleClass.SampleClass.return2());

In addition to that, a caller can create an instance for the class even though the class doesn’t have any function. It means that the instance can do nothing with it. It’s strange implementation.

{
    const obj = new SampleClass();
    console.log(obj instanceof SampleClass);
    // true
}

Test looks like this. Don’t mention “what are you testing?” This is just a test to check whether we can replace the function with stub or not.

describe("SampleClass", () => {
    it("should be able to stub", () => {
        sinon.stub(SampleClass, "return2").returns(99);
        const result = SampleClass.return2();
        expect(result).to.equal(99);
    });
});

Static function in an abstract class

To solve the problem above, we can use abstract class instead. A caller cannot create an instance for it in this way. It’s better than the previous way.

// SampleAbstractClass.ts
export abstract class SampleAbstractClass {
    public static return1() {
        return 1;
    }
}

// app.ts
import { SampleAbstractClass } from "./SampleAbstractClass";
console.log(SampleAbstractClass.return1());
// 1

// Error
// const obj = new SampleAbstractClass();

Its test looks the same as previous one.

describe("SampleAbstractClass", () => {
    it("should be able to stub", () => {
        sinon.stub(SampleAbstractClass, "return1").returns(99);
        const result = SampleAbstractClass.return1();
        expect(result).to.equal(99);
    });
});

Function in Namespace

Last option that I explain in this post is using namespace but I don’t know when to use namespace. It may be useful when we want to split the code into several files but put them all into one namespace. However, we should consider re-structuring it in this case.

Anyway, the usage is the same as others.

// NS.ts
export namespace NS {
    export function return4(){
        return 4;
    }
}

// app.ts
import { NS } from "./NS";
console.log(NS.return4());
// 4

I think using namespace doesn’t make much sense because we can import a whole file and name it. newName becomes kind of namespace in the following case. Then, we should use Top level function instead.

import * as newName from "./something";

The test looks the same as others.

describe("Namespace", () => {
    it("should be able to stub", () => {
        sinon.stub(NS, "return4").returns(99);
        const result = NS.return4();
        expect(result).to.equal(99);
    });
});

Conclusion

I recommend using Top level function unless it’s impossible to stub the function for some reason because it is the simplest way. Even when a caller imports the whole file the caller can call the function without awkward manner.

Comments

Copied title and URL