One of the weird things I find about TypeScript is that interface allow for optional properties. Should this be a feature you should use? Absolutely not!
An interface is a data contract. Once you add optional properties on that data contract, I can no longer trust the integrity of the class that implements the interface.
For this article, I'm going to explain the type of problem I use interfaces to solve, and why optional properties ruin them.
Why would I use an interface?
Let's pretend I'm tasked by my project with lead with creating a UI component to display an image and some additional image metadata. We get an object with all the relevant properties from some external system, SystemOne. Pretend we have no control or input into the data model. Our component must accept this object as a property and display the output. Here is a sample object from SystemOne:
2 id: '123',
3 image: 'urlToImage',
4 title: 'First Image',
5 creator: 'Me!',
6 createdDate: '8/28/2023'
7}
There are four pieces of data in this object. We create the component, something like this:
2<img src="{{image}}"/>
3<p>Created by {{creator}} on {{createdDate}}</p>
I'm using pseudo code, but the template syntax here is similar enough in both Angular and React.
You build this component and everything is good. For a moment. Then you're told you have to use the component with data from SystemTwo. Here is SystemTwo's object:
We have all the same data, but completely different names. Our component implementation will not work. We could convert objects from SystemTwo to the same schema from SystemOne and things would work fine. But, more often than not I'll see conditionals added to the component:
2<img src="{{image ? image : imageUrl }}"/>
3<p>Created by {{creator ? creator : author}} on
4 {{createdDate ? createdDate : date}}</p>
This is functional, but what happens when you have a third schema from a third data source? The code becomes difficult to extend and starts to get sloppy.
Solve the issue with an Interface
I can simplify the UI display code by using an interface and classes. First create an interface:
2 image: string;
3 title: string;
4 creator: string;
5 createdDate: string;
6}
In a real world application, I suspect the createdDate property would be a Date object, but for simplicity of this sample I'll keep it as string.
I'm going to create a class for data from SystemOne:
2 image: string = '';
3 title: string = '';
4 creator: string = '';
5 createdDate: string = '';
6}
This one is the easy one, because there is a 1:1 mapping from the interface to data in SystemOne. The SystemTwo class requires a bit more complexity:
2 id: string = '';
3 imageUrl: string = '';
4 englishTitle: string = '';
5 author: string = '';
6 date: string = '';
7 get title(): string {
8 return this.englishTitle;
9 }
10 get image(): string {
11 return this.imageUrl;
12 }
13 get creator(): string {
14 return this.author;
15 }
16 get createdDate(): string {
17 return this.date;
18 }
19}
To implement the interface properties, I created getter and setter methods for each interface property, which are just wrappers to return the differently named data property.
When loading data from each respective system, you'll need to convert the generic JSON objects into the formal classes. I like to use object.assign() to do so, possibly like this:
2 (value) => Object.assign(new SystemOneMetadata(), value)
3);
Or, from SystemTwo:
2 (value) => Object.assign(new SystemTwoMetadata(), value)
3);
Now, with these objects, the input into our display component is a ImageMetadataInterface and our display template is a lot simpler:
2<img src="{{image}}"/>
3<p>Created by {{creator}} on {{createdDate}}</p>
The display code is simplified, and the component is really simple and easily extensible. Let's show an example of that.
What happens when you add a third Data Type?
Now let's pretend we've been tasked to integrate with a brand new, third system. Here is a piece of sample data from the system:
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. What do we do about these? Most likely we'll get some business rules. For example, the image URL we should always display the thumbnail. That is easy enough, but the title is more difficult.
You should display the main title. 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 else display nothing. It is complicated precedence logic due to the complexity of the data structure.
Believe it or not, I deal with this sort of data all the time.
First, we have some embedded objects. Let's turn them into classes. The Title first:
2 title: string = '';
3 purpose: string = '';
4}
And then the image:
2 imageLocation: string = '';
3 purpose: string = '';
4}
Now, we can create the SystemThreeMetadata class. I'm going to with the basic fields.
2 id: string= '';
3 images: SystemThreeImage[] = [];
4 titles: SystemThreeTitle[]= [];
5 creator: string = '';
6 createdDate: string= '';
7}
This does not yet fully implement the interface, because we do not have image or title properties. For these, we're going to create getters that will return a string. Here is the image one:
2get image(): string {
3 if (this._image !== '') {
4 return this._image;
5 }
6 const thumbnail: SystemThreeImage[] = this.images.filter(
7 (image) => image.purpose = 'thumbnail'
8 );
9 if (thumbnail.length) {
10 this._image = thumbnail[0].imageLocation;
11 return this._image;
12 }
13 return '';
14}
First, I created a private variable, _image to contain the calculated image. If this value is already set, there is no need to re-run the calculations to determine the proper image URL. Then we perform a filter on the images array to get all the thumbnail images. If anything is returned, we get the imageLocation variable from the first element in the array. If no thumbnails are returned, an empty string is returned.
The image calculation is relatively straight forward, but the title one becomes more complex because it has a lot more fallbacks in image logic:
2get title(): string {
3 if (this._title !== '') {
4 return this._title;
5 }
6
7 if (!this.titles.length) {
8 return '';
9 }
10
11 const mainTitle: SystemThreeTitle[] = this.titles.filter(
12 (title) => title.purpose = 'main');
13 if (mainTitle.length) {
14 this._title = mainTitle[0].title;
15 return this._title;
16 }
17
18 const UKMainTitle: SystemThreeTitle[] = this.titles.filter(
19 (title) => title.purpose = 'UK Main');
20 if (UKMainTitle.length) {
21 this._title = UKMainTitle[0].title;
22 return this._title;
23 }
24
25 const altTitle: SystemThreeTitle[] = this.titles.filter(
26 (title) => title.purpose = 'alt');
27 if (altTitle.length) {
28 this._title = altTitle[0].title;
29 return this._title;
30 }
31
32 this._title = this.titles[0].title;
33 return this._title;
34}
This code is similar in approach, starting with a private _titles variable. If that variable is already defined, do not perform the logic calculations to find the title. Then it checks to see if there are any titles in the title array. If not, return an empty string.
Then we get into the array processing. First, we use the array filters() method to check for a main title. Does one exist? IF so, use it and return it. If not; perform a check for a UK Main title. If that doesn't exist, return the alt title. If that doesn't exist use the first title in the title array.
I like to keep logic like this out of my view code.
Final Thoughts!
By conforming all my incoming objects to an interface, the display code works very simply with no required changes when we add additional data. Without the use of the interface, I'm stuck writing complicated conditionals within my display code. I prefer to keep that business logic out of the display code.
Once we add optional properties to the Interface my primary use case for using one goes away; and I'm back to writing conditionals to check if a property exists on my objects or not. It ruins the sanctity of the data contract and makes it so I cannot rely on the interface.
As such, please don't use optional properties on Interfaces.
I threw together a working Angular sample that demonstrates the approaches detailed here.