This article is part of Programming TypeScript architecture series.
In this guide I’ll introduce an utility concept and how to implement it with TypeScript.
This pattern can be used for any type of software – eg. frontend or backend, or even inside Nginx’s NJS or PostgreSQL’s plv8.
In it’s core it’s simply public functions in a namespace which share a common context.
export class FooUtils {
public static strip (value: string) : string {
return value.replace(/ +/, "");
}
}
export default FooUtils;
…and use it:
import FooUtils from './FooUtils.ts';
FooUtils.strip('a b c');
What makes a class utility class is the fact that it has no internal state except the values you provide to it as arguments.
Eg. it shouldn’t use any other internal state which may change the result of the function. Hence, constants are fine to use, but global variables are not.
The second important part of the architecture is the context. You should place your functions based on a common context. Then you’ll know where to look when you need it next time – and so does your IDE. It also makes it easier to find a place to put your next new function.
import
/export
?This would be of course the standard EcmaScript way. It’s also completely fine to do so. Just be consistent, unless there’s a better reason not to be.
In this case you would just create a file fooUtils.ts
like:
export function strip (value: string) : string {
return value.replace(/ +/, "");
}
…and use it simply:
import fooUtils from './fooUtils.ts';
fooUtils.strip('a b c');
There are actually some benefits like the fact a correctly implemented ES import system can include only parts which you imported and let others uncompiled. However, with a correctly configured minifizing build system it should happen for unused parts of classes also.
The most likely problem with this style is that it relies on files to group functions together. The static class style can be compiled as a single file and debugged without loosing the context of grouped functions.
Yes, TypeScript has an actual namespace concept, which you’re of course free to utilize in the same manner, but I find a class much simpler concept for this particular use case.
Especially I like the class style because of private, protected and public methods and generally much wider support – eg. namespaces are not supported directly in ES systems like NodeJS and other Google v8 -based systems (at the time when this article was written).
There is also this article about avoiding namespaces, since it may be unlikely namespaces are introduced to the standard while there’s already a concept to implement them. It’s of course just speculation. You’re always free to go with actual namespaces. Just be consistent.
This is a good question. In the end it’s just an architectural design pattern, which probably looks useless in a small and minimal application.
However, for larger projects with multiple people working on the same code, these small architectural choices are much more important, and may even have an impact on how the project will succeed and how often [git] conflicts are made.
So, what is the point of an utility class? In the end, like most architectural choices, it’s just a way to divide different parts of the code into smaller files.
Let me explain with some examples from (almost) real world applications.
Let’s assume you’re developing a frontend application that uses a backend. You get the specification which kind of JSON the backend uses – you didn’t design it, it was made by another team or even another company.
UserDTO
. It describes an common interface for any user JSON object which
comes from the backend. TypeScript makes it easy to define types for any JSON object.
export interface UserDTO {
readonly firstName : string;
readonly lastName : string;
readonly isAdmin : boolean;
}
export function isUserDTO (value : any) : value is UserDTO {
return (
!!value
&& isString(value?.firstName)
&& isString(value?.lastName)
&& isBoolean(value?.isAdmin)
);
}
UserModel
. It describes an common interface for our internal user objects which
are used inside our application.
export interface UserModel {
getFirstName() : string;
getLastName() : string;
getDisplayName() : string;
isAdmin() : boolean;
}
Take a note, interfaces cannot include these static methods which we define next in UserUtils.
UserUtils
. These static functions work with any object implementing the User
interface, and may contain functions to parse backend responses. Unless the interface changes,
your team does not have to make changes to these functions when your internal
implementations are changed or new ones are created.
export class UserUtils {
public static parseUserDTO (value: any) : UserDTO {
if (!isUserDTO(value)) throw new TypeError('Argument was not UserDTO');
return value;
}
public static isAdminUserDTO (value: UserDTO) : boolean {
return value.isAdmin;
}
public static getFullNameFromDTO (model: UserDTO) : string {
return model.firstName + ' ' + model.lastName;
}
public static getFullNameFromModel (model: UserModel) : string {
return model.getFirstName() + ' ' + model.getLastName();
}
}
AdminUserModel
and CustomerUserModel
, etc.
These all implement the same UserModel interface, but different features. These classes may even
use UserUtils implementations to implement common methods. There are no circular dependency problems.
export class CustomerUserModel implements UserModel {
private readonly _firstName : string;
private readonly _lastName : string;
constructor (firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}
public getFirstName () : string {
return this._firstName;
}
public getLastName () : string {
return this._lastName;
}
public getDisplayName () : string {
return UserUtils.getFullNameFromModel(this);
}
public isAdmin() : boolean {
return false;
}
}
export class AdminUserModel implements UserModel {
private readonly _firstName : string;
private readonly _lastName : string;
constructor (firstName: string, lastName: string) {
this._firstName = firstName;
this._lastName = lastName;
}
public getFirstName () : string {
return this._firstName;
}
public getLastName () : string {
return this._lastName;
}
public getDisplayName () : string {
return UserUtils.getFullNameFromModel(this);
}
public isAdmin() : boolean {
return true;
}
}
export class UserService {
public static async fetchUser (id : string) : Promise<UserModel> {
const response : HttpResponse<UserDTO> = await HttpService.get(USER_API_URL(id));
return this._parseUserData(response?.data);
}
private static _parseUserData (dtoJson: any) : UserModel {
const dto : UserDTO = UserUtils.parseUserDTO(dtoJson);
if (UserUtils.isAdminUserDTO(dto)) {
return new AdminUserModel(dto.firstName, dto.lastName);
} else {
return new CustomerUserModel(dto.firstName, dto.lastName);
}
}
}
So in the end these architectural choices are just decoupling different parts of the software into different blocks in order to let different developers easily maintain and work on the same source code separately.
It also reduces duplicate source code lines in the application, which may reduce bugs, since the fix will not be forgot to apply to a duplicated code somewhere else in the app.