I recently wrote an article about interfaces and why they should never have optional properties on them. I created classes to implement an interface, with getter and setter methods to complete the API Contract. In an unrelated conversation on reddit, of all places, I went deep into a conversation on classes vs types. That made me think "Well, how would this work with types?"
What is the difference between a Class and a Type?
In short:
- A class is a run time entity.
- A Type is compile time entity!
If you create your objects to a class, then you'll get a strongly typed object in the browser.
Using classes is important if you want methods on the class, including getters and setters. But, if you're just using classes as generic data containers, then it probably doesn't matter.
The Use Case for Types
I want to imagine a scenario where you're getting data from multiple sources; each with a different data model. Your job is to mash up the data and display it as one entity. This is a very common use case in my world. This is a sample from SystemOne:2 id: '123',
3 image: 'urlToImage',
4 title: 'First Image',
5 creator: 'Me!',
6 createdDate: '8/28/2023'
7}
You can create view code to display this pretty easy.
Now, let's add in a sample from SystemTwo:
2 id: '123',
3 imageUrl: 'urlToImage',
4 englishTitle: 'First Image',
5 author: 'Me!',
6 date: '8/28/2023'
7}
This has a lot of the same data, but completely different property names. The way I see a lot of people do this is by adding conditionals in the view code to determine which value to display. That only works up to a point. Let's look at SystemThree:
2 images: [
3 { imageLocation: `urlToImage'`, purpose: 'header'},
4 { imageLocation: `urlToImage`, purpose: 'thumbnail'},
5 ],
6 titles: [
7 { title: `First Image Main Title, System Three`, purpose: 'main'},
8 { title: `First Image Alt Title, System Three`, purpose: 'alt'},
9 { title: `First Image UK English, System Three`, purpose: 'UK Main'},
10 ],
11 creator: `System Three`,
12 createdDate: `8/28/2023`
13}
We just lost a lot of code simplicity. Neither the image URL nor title are strings anymore. They are arrays that must be processed. Most likely we'll get some business rules, along the way. For example, the image URL we should always display the thumbnail. That is easy enough, but the title is more difficult, as we implement fallback logic.
You should display the main title first. But, if that isn't available, then display the UK Main Title. If that isn't available, display the alt title. If that isn't available, then display the first title in the array. Or if the title array has zero items in it, display nothing. It is complicated precedence logic due to the complexity of the data structure.
In the previous article I created an interface and three separate objects. The logic for parsing these items was placed within objects. SystemOne objects were simple. SystemTwo Objects used getter methods to implement the interface and return the proper value. SystemThree objects used getter methods, with all the parsing logic. The service to load data from the appropriate system would convert the results into objects. We had super simple UI Display code:
2<img src="{{image}}"/>
3<p>Created by {{creator}} on {{createdDate}}</p>
But, a thread on reddit suggested I investigate types to solve the same issue.
How to use Types?
The goal here is to convert all three objects from the data sources to specific types and then our UI Code can operate off that type. Start by creating the type:
2 id: string;
3 image: string;
4 title: string;
5 creator: string;
6 createdDate: string;
7};
We have 5 separate properties, that we'll need to pull out from the different systems. When loading data from SystemOne; it is pretty easy to do, since there is an easy 1:1 parallel between the SystemOne objects and the type. I have a method like this:
2 const newArray: MetadataType[] = [];
3 this.results.forEach((value) => {
4 newArray.push({
5 id: value.id,
6 image: value.image,
7 title: value.title,
8 creator: value.creator,
9 createdDate: value.createdDate
10 });
11 });
12 return newArray;
13}
The code loops over all the results, and creates a brand new object, representing the type, with the values from the SystemOne object.
The SystemTwo processing code is not much different:
2 const newArray: MetadataType[] = [];
3 this.results.forEach((value) => {
4 newArray.push({
5 id: value.id,
6 image: value.imageUrl,
7 title: value.englishTitle,
8 creator: value.author,
9 createdDate: value.date
10 });
11 });
12 return newArray;
13}
The main difference here is the mapping. The SystemTwo imageUrl value is set to the image property, and the englishTitle is set to the title property.
Dealing with SystemThree data becomes a bit more complex. First, I'm going to create a method to determine the proper image URL from the images array:
2 const thumbnail: SystemThreeImage[] = images.filter((image: any) => image.purpose = 'thumbnail');
3 if (thumbnail.length) {
4 return thumbnail[0].imageLocation;
5 }
6 return '';
7}
If filters the images, and finds all the images where the purpose is a thumbnail. It returns the imageLocation from the first one, or if none exists returns an empty string.
I'm going to need some code to calculate the title too:
2 if (!titles.length) {
3 return '';
4 }
5
6 const mainTitle: SystemThreeTitle[] = titles.filter((title: any) => title.purpose = 'main');
7 if (mainTitle.length) {
8 return mainTitle[0].title;
9 }
10
11 const UKMainTitle: SystemThreeTitle[] = titles.filter((title: any) => title.purpose = 'UK Main');
12 if (UKMainTitle.length) {
13 return UKMainTitle[0].title;
14 }
15
16 const altTitle: SystemThreeTitle[] = titles.filter((title: any) => title.purpose = 'alt');
17 if (altTitle.length) {
18 return altTitle[0].title;
19 }
20
21 return titles[0].title;
22}
It accepts an array of titles, and processes them. Checking all the various fallback options until they are exhausted. First looking for the main title. Then the UK Main title. Then the alt title. IF none of those are found, the first title is returned. Back to the top of the method, we check to make sure that there are some titles in the array and return an empty string if the array is empty.
That's great, now let's write our conversion code from a SystemThree object to the type:
2 const newArray: MetadataType[] = [];
3 this.mockDataSource.forEach((value) => {
4 newArray.push({
5 id: value.id,
6 image: this.calculateImage(value.images),
7 title: this.calculateTitle(value.titles),
8 creator: value.creator,
9 createdDate: value.createdDate
10 });
11 });
12 return newArray;
13}
This one is as simple as the previous. Date, Creator, and id are all just mappings. But, image and title, call the processing methods for each item.
Using the Types
At some point, you'll load data from all three systems and mash them together. I have code like this in my sample:
2 ...this.systemTwoMockService.getTypeObjects(),
3 ...this.systemThreeMockService.getTypeObjects()];
Our display code, in Angular, would accept an input like this:
With the data type being the MetadataType. Our display code is super simple:
2<img src="{{data?.image}}"/>
3<p>Created by {{data?.creator}} on {{data?.createdDate}}</p>
All the complexity of dealing with the different data types is encapsulated way from the view code. In this case, I put it in the services I used to load the daa.
Interfaces and Classes vs Types
After all is said and done, I think I prefer the class and interface approach that I wrote about in the previous article. I think it does better of encapsulating the data processing code; while also keeping the primary data object untouched, in case it's original form is needed in some manner. But, this was a good exploration into types an area of TypeScript I never delved deep into, but I'm glad I got a chance to here.