
TypeScript 4 Design Patterns and Best Practices
By :

You now know how to work with VSCode and have a firm understanding of its code base and some examples. We will complete this chapter by learning about UML and how we can utilize it to study design patterns. We will focus on a limited set of UML, specifically class diagrams, since this is the traditional way to depict design patterns; plus, they are straightforward to comprehend.
UML is a standardized way of modeling software architecture concepts, as well as interactions between systems or deployment configurations. Nowadays, UML covers more areas, and it's fairly comprehensive. It came as a result of a consolidation of similar tools and modeling techniques, such as use cases, the Object Modeling Technique (OMT), and the Booch Method.
You don't really need to know all the ins and outs of UML, but it is really helpful when you're learning about design patterns. When you first learn about design patterns, you want to have a holistic overview of the patterns, irrespective of the implementation part, which will differ from language to language. Using UML class diagrams is a perfect choice for modeling our patterns in a design language that everyone can understand with minimal training.
Let's delve into more practical examples using TypeScript.
Note
Although UML diagrams have a long history in software engineering, you should use them carefully. Generally, they should only be used to demonstrate a specific use case or sub-system, together with a short explanation of the architecture decisions. UML is not very suitable for capturing the dynamic requirements of very complex systems because, as a visual language, it is only suitable for representing high-level overviews.
UML class diagrams consist of static representations of the classes or objects of a system. TypeScript supports classes and interfaces, as well as visibility modifiers (public
, protected
, or private
) so that we can leverage those types to describe them with class diagrams. Here are some of the most fundamental concepts when studying class diagrams:
Product
class looks like this:class Product {}
This corresponds to the following diagram:
Figure 1.7 – Class representation
interface Identifiable<T extends string | number>{ id: T } class Product implements Identifiable<string> { id: string constructor(id: string) { this.id = id; } }
This corresponds to the following diagram. Notice the placement of the interface clause on top of the class name within the left shift (<<
) and right shift (>>
) symbols:
Figure 1.8 – Interface representation
abstract class BaseApiClient {}
This corresponds to the following diagram. The name of the class is in italics:
Figure 1.9 – Abstract class representation
Blog
and Author
:class Blog implements Identifiable<string> { id: string; authorId: string; constructor(id: string, authorId: string) { this.id = id; this.authorId = authorId; } } class Author {}
This corresponds to the following diagram. Blog
is connected to Author
with a line:
Figure 1.10 – Association representation
Notice that because the Author
class here is not being passed as a parameter, it is referenced from the authorId
parameter instead. This is an example of indirect association.
SearchService
that accepts a QueryBuilder
parameter and performs API requests on a different system:class QueryBuilder {} class EmptyQueryBuilder extends QueryBuilder {} interface SearchParams { qb?: QueryBuilder; path: string; } class SearchService { queryBuilder?: QueryBuilder; path: string; constructor({ qb = EmptyQueryBuilder, path }: SearchParams) { this.queryBuilder = qb; this.path = path; } }
This corresponds to the following diagram. SearchService
is connected to QueryBuilder
with a line and a white rhombus:
Figure 1.11 – Aggregation representation
In this case, when we don't have a QueryBuilder
or the class itself has no queries to perform, then SearchService
will still exist, although it will not actually perform any requests. QueryBuilder
can also exist without SearchService
.
Composition is a stricter version of aggregation, where we have a parent component or class that will control the lifetime of its children. If the parent is removed from the system, then all the children will be removed as well. Here is an example with Directory
and File
:
class Directory { files: File[]; directories: Directory[]; constructor(files: File[], directories: Directory[]) { this.files = files; this.directories = directories; } addFile(file: File): void { this.files.push(file); } addDir(directory: Directory): void { this.directories.push(directory); } }
This corresponds to the following diagram. Directory
is connected to File
with a line and a black or filled rhombus:
Figure 1.12 – Composition representation
class BaseClient {} class UsersApiClient extends BaseClient {}
This corresponds to the following diagram. UsersApiClient
is connected to BaseClient
with a line and a white pointed arrow:
Figure 1.13 – Inheritance representation
SSHUser
class that accepts a private key and a public key:class SSHUser { private privateKey: string; public publicKey: string; constructor(prvKey: string, pubKey: string) { this.privateKey = prvKey; this.publicKey = pubKey; } public getBase64(): string { return Buffer.from(this.publicKey).toString ("base64"); } }
This corresponds to the following diagram. SSHUser
contains two properties and one method. We use a minus (-
) for private visibility and a plus (+
) for public visibility:
Figure 1.14 – Visibility
Here, we can see that the methods are separated by a horizontal bar for visibility.
We can also add notes or comments to class diagrams, although it's not very clear if they should be included in the code:
Figure 1.15 – Comments representation
The main difficulty when using class diagrams is not drawing them on a piece of paper, but rather how to properly model the domain classes and relationships in a sound manner. This process is often iterative and involves interacting with several domain experts or knowledgeable stakeholders. In Chapter 8, Developing Modern and Robust TypeScript Applications, we are going to learn how domain-driven design can help us with modeling business rules.
Change the font size
Change margin width
Change background colour