The first glance at Typescript
So, recently I joined to a project with Typescript and last a few months I worked exactly with Typescript programming language.
Of course, I took this opportunity with great enthusiasm! So much talks about Typescript in a community, so much libraries and snippets! Strong typing is that we really need in Javascript world! Or not? Perhaps do we need something else? But what does TS suggest us exactly?
I would like to understand motivation before using new programming language in real projects.
I guess I found an answer for main question "Why?" in first paragraphs of TypeScript documentation
The goal of TypeScript is to be a static typechecker for JavaScript programs - in other words, a tool that runs before your code runs (static) and ensures that the types of the program are correct (typechecked).
Hm, not bad, it means that a Goal is just static code analysis. Ok, lets make it as a start proint and will not not expect more from the language.
Also TypeScript declares sull support of classes and interfaces.
Wait a minute, what?
Interfaces and classes?!?!
Now it looks like completed object-oriented programming language! Wow, if I will be able to use inheritance, polymorphism and OOP patterns? Generally - yes! In practice I faced the fact that community doesn't do it!
I asked that question to my team and at social networks, but the best asnwer was:
Class is just an overhead to be data descriptor, because typescript uses structural typing.
Hm, honestly, I don't understand why it is an overhead?
Let's take a look at an example.
Say we have two different entities from server side which should be rendered in UI.
The first entity is User:
{
"firstName": "Jhon",
"lastName": "Doe",
"address": "London, UK"
}
The second one is Organization:
{
"name": {
"fullName": "MissionInmossible Incorparated.",
"shortName": "MI Inc."
}
"address": {
"legal": "M60 2LA Manchester, UK",
"branches": ["London, UK"]
}
}
We should render both of them into one UI component, for instance:
First, I have to say I don't want to create 2 very similar components - remember about old man Ockham.
So, in my mind I have several ways to solve that issue:
1. Data mapping
Lets define interfaces for our entities and create separated interface for component, then map them:
interface IUser {
firstName: string;
lastName: string;
address: string;
};
interface IOrganization {
name: {
fullName: string;
shortName: string;
};
address: {
legal: string;
branches: Array<string>;
}
};
interface IUIListItem {
name: string;
address: string;
}
interface IUIListProps {
items: Array<IUIListItem>
};
function UIList({items}: IUIListProps) {
}
const user: IUser = {
"firstName": "Jhon",
"lastName": "Doe",
"address": "London, UK"
};
const organization: IOrganization = {
"name": {
"fullName": "MissionInmossible Incorparated.",
"shortName": "MI Inc."
}
"address": {
"legal": "M60 2LA Manchester, UK",
"branches": ["London, UK"]
}
}
const items: Array<IUIListItem> = [
{
name: `${user.firstName} ${user.lastName}`,
address: user.address
},
{
name: organization.name.fullName,
address: organization.address.legal
}
];
<UIList items={items}/>
Exaclt such approach I can see in most code in Internet....probably I see at rong place.
This approach has obvious and very large drawbacks.
First, the extra time and memory costs. If the list of elements is large enough, and we need to map each of them, the operation can become quite expensive.
Second, if we decide to bring new entity - should map it as well.
Thirdly, and this is the most painful thing, if the structure of the entity changes, it will also be necessary to edit in the mapper scripts. The same works in the other direction, on the component changing.
2. Guard
Ok, let's don't create the separate interface. Instead, pass our entites as a prop into the component with union orepator.
I leave piece of code in the example above.
Here
Here, for clarity, I use the UIListItem component.
interface IUIListItemProps {
item: IUser | IOrganization
};
function UIListItem({item}: IUIListItemProps) {
let name = '';
if (item.name) {
name = item.name.fullName;
} else {
name = item.firstName;
}
}
<UIListItem item={user}/>
<UIListItem item={organization}/>
If I got it correctly, it is something like (although in this example it is clumsy, but more visually) guard mechanism looks like.
Well, this approach I like more. At least we don't need to create extra objects. But a huge problem of this approach is the incredible connectivity of data and views. If we'll need to add another one entity into the product - we'll need to modify all components use that entity.
My third solution follows from that "connectivity" problem.
3. Good old OOP
I mentioned OOP and design patterns. Let's look if we can use something that was invented many many years ago.
For example separate components and data via an interface. To do this, we should define entities as classes which implement that components's interface.
interface IUIListItem {
getName: () => string;
getAddress: () => string;
}
class User implements IUIListItem {
firstName: string;
lastName: string;
address: string;
constructor() {
this.firstName = '';
}
getName() {
return this.firstName;
}
};
class Organization implements IUIListItem {
name: {
fullName: string;
shortName: string;
};
address: {
legal: string;
branches: Array<string>;
}
constructor() {
this.name = {
fullName: 'dasdasdas'
}
}
getName() {
return this.name.fullName;
}
};
interface IUIListItemProps {
item: IUIListItem
};
function UIListItem({item}: IUIListItemProps) {
let name = item.getName();
}
<UIListItem item={user}/>
<UIListItem item={organization}/>
I didn't implement both methods, because I think it is obviously, that we can do anything with our data, it will not affect to component. Here the best from OOP - component provides its own interface, if someone want to work with the component - just implement the interface!
Even if a new entity appears - we have to just implements the interface!
If the interface changes - ok, we need to modify classes, but data exaclty will be the same. We don't need touch it.
Personally, this approach is closest to me, but I am only at the beginning of the path.
Perhaps you, like me, have a reasonable question: "Wait, wait, but in order to use the methods of the class, we need to create an object through new
. And this assumes that we must somehow initialize the variables inside the class. Here it is overhead right?"
On the one hand, not at all!
С одной стороны вообще не так! Data is not taken from nowhere! We can eiher get it from fetch request, or from database, or create ourself in the code. In any cases we can do it through new
.
But on the other side, exaclty in Typescript we have to do it manually millions of times! And this is the biggest mystery for me!
What is wrong with class' instances?
For example, I want to get data from a request:
const result = await fetch(`some-url`);
class MyData() {
name: string;
constructor() {
this.name = '';
}
}
const data: MyData = await result.json();
As a result I want to get instance of class MyData
with filled fields inside data
variable. In fact I got just object from the answer. Note, that data instanceof MyData
will be false
. I agree that in order to fill fields we need some kind of decorators which will be mapping fields and answer's json, but I'm sure it is possible! But why I don't receive an instance of class??? Even with incomplete or incorrect data, but if I define my variable as 'class' I want to get instance of that 'class'!
This is very strange behavior which, in fact, kills OOP in Typescript
And yes, I know hte histry of Javascript and Typescript, and yes, I understand all difficulties related with classes etc...
But may be we don't need that functionality at all and we'll use just static analizator? In this case why we need a separate language? I can't answer to myself for now.
Magic
So last thing I want to mention is utility types. At first sight they looks like absolutely magic. It is completely unclear why we need them and what exaclty they do. Later I noticed that often we use them like an alternative for design patterns. Instead of creating interfaces and fabrics we manipulate and combine utility types. It looks strange and unusual. Although I found some pleasure in it.
Conclusions
After several months of working with Typescript I still can't decide if I like it and if I will use it on my next pet project. I really like static analysis and strong typing. But I like it in Java as well. And Java has complete OOP, but Typescript has it with reservation.
On the other hand, it is necessary to compare not with Java, but with Javascript. And here the question is deeper and more interesting, especially in light of the latter's obvious thrust in functional programming.
As I said, I'm only at the beginning of the journey, maybe in a couple of months I'll be completely delighted with Typescript.