18th April 2025
Typescript makes you think
I definitely have an ambivalent relationship with Typescript, but I am quite clear on the benefits it brings to my development process.
The ambivalence comes with the desire to be, sometimes, spontaneous, creative in the moment, to extemporise and to follow inspiration wherever it might lead. Or just to be lazy. Typescript to some extent limits those tendencies, in that everything acquires an additional step, either because of the need to define types, or more often, to work around type definitions which have previously been created. It forces the programmer to confront structural questions.
Also, when using Typescript, code loses its clean lines. It becomes more verbose. Keyboard characters jostle for meaning, depending on where they are applied. Abstractions are rife. At times, the whole thing becomes arcane, obscuring the logic of the code, which might previously have been discernable in the code structure, in its sentences, phrases and syllogisms.
However, it is inevitable in a complex app that even after any improvisory moment, a process of careful structuring and organising will be required. The virtue of Typescript is that it forces you to develop a more formal structure and stick to it. It forces you to do 'slow thinking'. Sometimes that feels onerous. But that careful building of structure is, like it or not, what programming really is.
What is normal?
Web applications are often complicated because they have multiple possible points of interaction. A value can be updated in a sidebar, a content panel or a pop-up box. There are numerous points of entry, unlike a game which runs a single process at 30 frames per second in a loop. So it is vital to have a single source of truth and to apply updates consistently, so that the most recent change is universally applied. That means (for me at least) having a centralised 'store' of data values in the application.
I was finding, in spite of adopting this architecture, that in Orsn, my handling of data was leading to inconsistencies and some errors. It turned out that this was because my data store was not really acting as a single 'source of truth'. Why not?
Data normalisation should be a familiar set of principles, even by intuition, to anyone who has worked with relational databases. They are principles which are designed to reduce redundancy and ensure data integrity. They prevent the same data from being saved in more than one place, which can lead to complexity, inconsistencies and errors. While observing such principles carefully in my database (on the server), I was not applying them consistently in my data store (on the user computer–usually called the 'client' but who's to say what this relationship is?).
To take a simple example:
In the database we have a table to store Cards, and a table to store Tags. Card have Tags. Multiple Tags in some cases. Tags belong to Cards. Multiple Cards in some cases. So it's a 'many to many' relationship. In such a scenario we need a third database table to store the relationship between Cards and Tags. But we don't save the whole Tag or Card (name, author, timestamp, etc.) to the third table: instead we store references to entries in the other tables. 'Tag number 1 is related to Card number 3.' etc.
Meanwhile, back at the app, we want to load up some Cards and their Tags. So we download the cards and tags from the database and put them in our data store. Thing is, it's quite easy to download 'whole' cards and their 'whole' tags as a single structured object from the database, and this is a convenient data model to use in the store. This way, every time I fetch a card from the store, I also have access to its tags, with all the tags' properties. That's fine... until I need to have a page in my app for managing all tags. In that case it makes sense to have a dedicated place in the store just for tags, which again I download from the database. Now we are in a bad position because 'complete' tags are stored in two places in my store, and if I update them in one place, I need to remember to update them in the other place too.
This unnormalised store structure is to some extent encouraged by the affordances of some code libraries for querying databases (I use objection.js for Orsn), as they allow for the easy downloading, in a single operation, of data objects and all their related objects as a single entity, even though they may be stored in different database tables. In some contexts, a store structure like this, with duplicate entries, might be manageable, but as complexity increases, so does the potential for errors.
Typescript to the rescue
Typescript is not really the saviour here, but when I adopted a more stringently normalised model for my data store, I was able to use Typescript to clarify the nature of that model and enforce its use throughout the development process.
The fact that Typescript is a superset of javascript, and a structural typing system as opposed to a nominal one, gives it flexibility suited for this use case. Instead of creating a completely new set of normalised data models to use in my store, I was able to selectively adapt the shape of the existing 'full' models, to be normalised ones:
export class Card implements SearchableModel {
static readonly fieldsToSearch = ['title', 'blocks.body'];
id: string;
title: string;
blocks: Pick<Block, 'id'>[];
tags: Pick<Tag, 'id'>[];
projects: Pick<Project, 'id'>[];
stacks: Pick<Stack, 'id'>[];
cardsStacks?: CardStacksProperty[];
isHighlighted?: boolean;
isNew?: boolean;
extensionInstances?: Pick<ExtensionInstance, 'id'>[];
createdById: string;
createdTime: string;
formattedCreatedTime?: string;
modifiedTime: string;
modifiedById: string;
accessedTime: string;
fieldsToSearch: string[];
constructor(
title = date.getNowFormattedForCardTitle(),
tags: Tag[] = [],
projects: Project[] = [],
stacks: Stack[] = [],
blocks: Block[] = [],
cardsStacks: CardStacksProperty[] = [],
createdById = '',
createdTime = '',
modifiedTime = '',
modifiedById = '',
accessedTime = ''
) {
this.title = title;
this.tags = tags.map((tag) => ({ id: tag.id }));
this.projects = projects.map((project) => ({ id: project.id }));
this.stacks = stacks;
this.cardsStacks = cardsStacks;
this.blocks = blocks.map((block) => ({ id: block.id }));
this.createdById = createdById;
this.createdTime = createdTime;
this.modifiedTime = modifiedTime;
this.modifiedById = modifiedById;
this.accessedTime = accessedTime;
this.fieldsToSearch = Card.fieldsToSearch;
}
}
Instead of having an array of full Tag objects in its tags
property, each Card now only stores the id
value of its Tags, in a direct reflection of the normalised server database structure. This is achieved using the Typescript Pick
feature, which allows for declaration of a partial type based on one previously declared. A nominal type system might have required the declaration of entirely new classes.
This means that when the app retrieves Cards from the cards store, it now has to get the related Tags from the tags store too. But this additional step can be incorporated into the Cards getter method in the cards store, so it doesn't lead to additional complexity in everyday use. With this approach data integrity in the store is much improved, and in this example, in spite of my previous complaints about its readability, the use of Typescript in the class definition is helpfully explanatory of how the data is structured.
Contact
Find me on the fediverse (Mastodon) @Lemmy@post.lurk.org, or send an email to: info [at] orsn.io