📜 ⬆️ ⬇️

Why use static types in javascript? (Advantages and disadvantages)

We talked about many things in the first part . Now with the syntax done, let's finally move on to the most interesting: to study the advantages and disadvantages of using static types.

Advantage # 1: You can find bugs and errors in advance.


Static type checking allows you to verify that the invariant defined by us is true , even without starting the program. And if there is any violation of these invariants, it will be detected before the launch of the program, and not during its operation.

A small example: suppose we have a small function that takes a radius and calculates the area:

 const calculateArea = (radius) => 3.14 * radius * radius; var area = calculateArea(3); // 28.26 

Now, if we want to pass functions to a radius that is not a number (of the “malefactor” type) ...
')
 var area = calculateArea('im evil'); // NaN 

We will return NaN . If any functionality is based on the fact that the function calculateArea always returns a number, this will lead to vulnerability or failure. Not very nice, right?

If we used static types, we would define a specific type of the passed parameters and return values ​​for this function:

 const calculateArea = (radius: number): number => 3.14 * radius * radius; 

Now try sending something besides the number of the function calculateArea - and Flow will return a convenient and nice message:

 calculateArea('Im evil'); ^^^^^^^^^^^^^^^^^^^^^^^^^ function call calculateArea('Im evil'); ^^^^^^^^^ string. This type is incompatible with const calculateArea = (radius: number): number => 3.14 * radius * radius; ^^^^^^ number 

Now we have a guarantee that the function will only accept valid input numbers and return the result only in the form of valid numbers.

Since the type controller reports errors to you right at the time of writing the code, it is much more convenient (and much cheaper) than finding a bug after the code has been sent to the customer.

Advantage # 2: You have live documentation.


Types work as living, breathing documentation for you and for others.

To understand how, look at the method that I once found in a large code base with which I worked:

 function calculatePayoutDate(quote, amount, paymentMethod) { let payoutDate; /* business logic */ return payoutDate; } 

At first glance (both the second and third), it is completely incomprehensible how to use this function.

Is the quote a number? Or a logical value? Is the payment method an object? Or can it be a string that represents the type of payment method? Does the function return a date in string form? Or is it a Date object?

No idea.

At that time, I decided to evaluate all business logic and did a grep on the code base until I found out everything. But this is too much work just to understand how a simple function works.

On the other hand, if I wrote something like this:

 function calculatePayoutDate( quote: boolean, amount: number, paymentMethod: string): Date { let payoutDate; /* business logic */ return payoutDate; } 

then it would immediately become clear which data type the function accepts and which type returns. This is an example of how static types can be used to communicate what the function intends to do. We can tell other developers what we expect from them, and we can see what they expect from us. Next time, if someone is going to use this function, there will be no questions.

It can be argued that this problem is solved by adding comments to the code or documentation:

 /* @function Determines the payout date for a purchase @param {boolean} quote - Is this for a price quote? @param {boolean} amount - Purchase amount @param {string} paymentMethod - Type of payment method used for this purchase */ function calculatePayoutDate(quote, amount, paymentMethod) { let payoutDate; /* .... Business logic .... */ return payoutDate; }; 

It works. But there are more words. In addition to verbosity, such comments in the code are difficult to maintain, because they are unreliable and have no structure — some developers write good comments, others may write something incomprehensible, and some may forget to leave a comment.

It is especially easy to forget to update the comment after refactoring. On the other hand, type annotations have clearly defined syntax and structure, and they will never become obsolete because they are encoded in the code itself.

Advantage # 3: The handling of entangled errors is fixed.


Types help eliminate code handling in entangled errors. Let's go back to our function calculateArea and see how this happens.

This time I will give her an array of radii to calculate the areas for each radius:

 const calculateAreas = (radii) => { var areas = []; for (let i = 0; i < radii.length; i++) { areas[i] = PI * (radii[i] * radii[i]); } return areas; }; 

This function works, but does not properly handle invalid input arguments. If we want to make sure that the function correctly handles situations where the input arguments are not valid arrays of numbers, then we arrive at a function of approximately the following form:

 const calculateAreas = (radii) => { // Handle undefined or null input if (!radii) { throw new Error("Argument is missing"); } // Handle non-array inputs if (!Array.isArray(radii)) { throw new Error("Argument must be an array"); } var areas = []; for (var i = 0; i < radii.length; i++) { if (typeof radii[i] !== "number") { throw new Error("Array must contain valid numbers only"); } else { areas[i] = 3.14 * (radii[i] * radii[i]); } } return areas; }; 

Wow. There is a lot of code for such a small piece of functionality.

And with static types, we just write:

 const calculateAreas = (radii: Array<number>): Array<number> => { var areas = []; for (var i = 0; i < radii.length; i++) { areas[i] = 3.14 * (radii[i] * radii[i]); } return areas; }; 

Now the function really looks like it looked before adding all the visual garbage due to error handling.

Easy to understand the benefits of static types, right?

Advantage # 4: You can refactor more confidently.


Let me explain this story of life. Once I worked with a very large code base, and I had to update the method installed in the User class. In particular, change one of the parameters of the function from string to object .

I made a change, but I was scared to send a commit — there were so many calls to this function scattered throughout the code that I was not sure that I had updated all the instances correctly. What if some call remained somewhere deep in an unverified auxiliary file?

The only way to check is to send the code and pray that it will not explode with a bunch of errors.

When using static types, this problem will not occur. There I will have peace and tranquility in my heart: if I update the function and type definitions, the type controller will be near and find all the errors I could miss. It only remains to go through these type errors and correct them.

Advantage # 5: Separation of data and behavior


One rarely mentioned advantage of static types is that they help separate data from behavior.

Look again at our function calculateArea with static types:

 const calculateAreas = (radii: Array<number>): Array<number> => { var areas = []; for (var i = 0; i < radii.length; i++) { areas[i] = 3.14 * (radii[i] * radii[i]); } return areas; }; 

Think about how we would approach the compilation of this function. Since we specify data types, we are forced first of all to think about the data types that we are going to use, so that we can set the types for the passed parameters and return values ​​accordingly.



Only after that we implement the logic:



The ability to accurately express the data separately from the behavior allows us to clearly indicate our assumptions and more accurately convey the intention, which removes a certain mental load and gives the programmer a certain clarity of mind. Otherwise, it remains to keep everything in the mind in some way.

Advantage # 6: Eliminating an entire category of bugs.


Errors of types at runtime are one of the most common errors or bugs encountered by JavaScript developers.

For example, suppose that the initial state of the application was set as follows:

 var appState = { isFetching: false, messages: [], }; 

And suppose then we make an API call to pick up messages and fill our appState . Further, our application has an oversimplified component for viewing, which takes messages (indicated in the state above) and displays the number of unread messages and each message as a list item:

 import Message from './Message'; const MyComponent = ({ messages }) => { return ( <div> <h1> You have { messages.length } unread messages </h1> { messages.map(message => <Message message={ message } /> )} </div> ); }; 

If the call to the API for collecting messages did not work or returned undefined , then you will encounter a type error in production:

 TypeError: Cannot read property 'length' of undefined 

... and your program will fail. You lose a customer. A curtain.

Let's see how static types can help. We start by adding Flow types to the application state. I use the AppState to determine the state:

 type AppState = { isFetching: boolean, messages: ?Array<string> }; var appState: AppState = { isFetching: false, messages: null, }; 

Since it is known that the API for collecting messages does not work reliably, we specify for the value of messages type of maybe for an array of strings.

Just like last time, we retrieve messages through an unreliable API and use them in the view component:

 import Message from './Message'; const MyComponent = ({ messages }) => { return ( <div> <h1> You have { messages.length } unread messages </h1> { messages.map(message => <Message message={ message } /> )} </div> ); }; 

But at this point, Flow will detect the error and complain:

 <h1> You have {messages.length} unread messages </h1> ^^^^^^ property `length`. Property cannot be accessed on possibly null value <h1> You have {messages.length} unread messages </h1> ^^^^^^^^ null <h1> You have {messages.length} unread messages </h1> ^^^^^^ property `length`. Property cannot be accessed on possibly undefined value <h1> You have {messages.length} unread messages </h1> ^^^^^^^^ undefined { messages.map(message => <Message message={ message } /> )} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly null value { messages.map(message => <Message message={ message } /> )} ^^^^^^^^ null { messages.map(message => <Message message={ message } /> )} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ call of method `map`. Method cannot be called on possibly undefined value { messages.map(message => <Message message={ message } /> )} ^^^^^^^^ undefined 

Wait, buddy!

Since we have defined messages as the type of maybe , we allow it to be null or undefined . But this does not give us the right to perform operations with it (like .length or .map ) without checking for null , because if the value of messages is actually null or undefined , a type error will pop up when trying to perform an operation with it.

So back and update our function to view something like this:

 const MyComponent = ({ messages, isFetching }: AppState) => { if (isFetching) { return <div> Loading... </div> } else if (messages === null || messages === undefined) { return <div> Failed to load messages. Try again. </div> } else { return ( <div> <h1> You have { messages.length } unread messages </h1> { messages.map(message => <Message message={ message } /> )} </div> ); } }; 

Now, Flow knows that we have considered all situations where messages are null or undefined , so the code type check ends with 0 errors. Goodbye errors during program execution!

Advantage # 7: Reducing the number of unit tests


Previously, we saw how static types help get rid of parsing entangled errors, because they guarantee the types of parameters and return values ​​that are passed to the function. As a result, static types also reduce the number of unit tests.

For example, back to our function calculateAreas with dynamic types and error handling.

 const calculateAreas = (radii) => { // Handle undefined or null input if (!radii) { throw new Error("Argument is missing"); } // Handle non-array inputs if (!Array.isArray(radii)) { throw new Error("Argument must be an array"); } var areas = []; for (var i = 0; i < radii.length; i++) { if (typeof radii[i] !== "number") { throw new Error("Array must contain valid numbers only"); } else { areas[i] = 3.14 * (radii[i] * radii[i]); } } return areas; }; 

If we were diligent programmers, we could think about testing invalid passed parameters to verify that they are correctly processed by our program:

 it('should not work - case 1', () => { expect(() => calculateAreas([null, 1.2])).to.throw(Error); }); it('should not work - case 2', () => { expect(() => calculateAreas(undefined).to.throw(Error); }); it('should not work - case 2', () => { expect(() => calculateAreas('hello')).to.throw(Error); }); 

… and so on. But it is very likely that we will forget to test some boundary cases - and our customer will be the one who finds the problem. :(

Since the tests are based solely on the situations that we have come up with to test, they are existential, and in practice they are easy to get around.

On the other hand, when we need to set types:

 const calculateAreas = (radii: Array<number>): Array<number> => { var areas = []; for (var i = 0; i < radii.length; i++) { areas[i] = 3.14 * (radii[i] * radii[i]); } return areas; }; 

... we not only get a guarantee that our goal corresponds to reality, but such tests are simply more reliable. Unlike empirical tests, types are universal and more difficult to circumvent.

If you look at the whole, the picture is as follows: tests are good for checking logic and types for checking data types. When they are combined, the sum of the parts gives an even greater effect.

Advantage # 8: The Domain Modeling Tool


One of my favorite examples of using static types is domain modeling. In this case, a model is created that includes both the data and the program behavior on this data. In this case, it is best to understand by example how to use types.

Suppose that the application offers the user one or more payment methods for making purchases on the platform. The user is allowed to choose from three payment methods (Paypal, credit card, bank account).

So, first apply the type aliases for the three payment methods:

 type Paypal = { id: number, type: 'Paypal' }; type CreditCard = { id: number, type: 'CreditCard' }; type Bank = { id: number, type: 'Bank' }; 

Now you can set the PaymentMethod type as a non-overlapping set with three cases:

 type PaymentMethod = Paypal | CreditCard | Bank; 

Now we will make a model of the state of our application. To keep things simple, suppose that these applications consist only of payment methods available to the user.

 type Model = { paymentMethods: Array<PaymentMethod> }; 

It is acceptable? Well, we know that to obtain user payment methods, you need to make a request to the API and, depending on the result and stage of the process, the application can take different states. In reality, there are four possible states:

1) We did not receive payment methods.
2) We are in the process of receiving payment methods.
3) We have successfully received payment methods.
4) We tried to get payment methods, but an error occurred.

But our simple Model type with paymentMethods does not cover all of these cases. Instead, it assumes that paymentMethods always exists.

Hmmmm Is there a way to make a model so that the state of the application takes one of these four values, and only them? Let's get a look:

 type AppState<E, D> = { type: 'NotFetched' } | { type: 'Fetching' } | { type: 'Failure', error: E } | { type: 'Success', paymentMethods: Array<D> }; 

We used the non-overlapping set type to set the AppState to one of the four states described above. Notice how I use the type property to determine which of the four states the application is in. This type property is what creates a non-intersecting set. Using it we can analyze and determine when we have payment methods, and when not.

You will also notice that I am passing the parameterized type E and D into an application state. Type D will be the user's payment method ( PaymentMethod , defined above). We have not set the type E , which will be our type for the error, so do it now:

 type HttpError = { id: string, message: string }; 

Now you can simulate the application domain:

 type Model = AppState<HttpError, PaymentMethod>; 

In general, the signature for the application state is now AppState<E, D> , where E is of the form HttpError , and D is the PaymentMethod . And the AppState has four (and only these four) possible states: NotFetched , Fetching , Failure and Success .



Such domain models seem to me useful for thinking and developing user interfaces in accordance with certain business rules. Business rules tell us that an application can only be in one of these states, and this allows us to explicitly present the AppState and ensure that it will only be in one of these predefined states. And when we develop on this model (for example, we create a component for viewing), it becomes absolutely obvious that we need to handle all four possible states.

Moreover, the code documents itself — just look at the disjoint sets, and it immediately becomes clear how AppState is structured.

Disadvantages of using static types


Like everything else in life and programming, checking static types requires some compromises.

It is important to understand and recognize these shortcomings so that we can make an informed decision when it makes sense to use static types and when they are simply not worth it.

Here are some of these considerations:

Deficiency # 1: Static types require preliminary study.


One reason why JavaScript is such a fantastic language for beginners is that beginners do not need to learn the complete type system before starting productive work.

When I first learned Elm (a functional language with static typing), types often got in the way. I constantly encountered compiler errors due to my type definitions.

Learning to use types effectively was half the success in learning the language itself. As a result, because of the static types, the Elm learning curve is cooler than JavaScript.

This is especially important for beginners who have the greatest cognitive load from the study of syntax. Adding syntax to this set can overwhelm the novice.

Disadvantage number 2: Can be bogged down in verbosity


Because of the static types, programs often look more verbose and cluttered.

For example, instead:

 async function amountExceedsPurchaseLimit(amount, getPurchaseLimit){ var limit = await getPurchaseLimit(); return limit > amount; } 

We have to write:

 async function amountExceedsPurchaseLimit( amount: number, getPurchaseLimit: () => Promise<number> ): Promise<boolean> { var limit = await getPurchaseLimit(); return limit > amount; } 

And instead:

 var user = { id: 123456, name: 'Preethi', city: 'San Francisco', }; 

I have to write this:

 type User = { id: number, name: string, city: string, }; var user: User = { id: 123456, name: 'Preethi', city: 'San Francisco', }; 

Obviously, extra lines of code are added. But there are a couple of arguments against this as a disadvantage.

First, as we mentioned earlier, static types destroy a whole category of tests. Some developers may find this a perfectly reasonable compromise.

Secondly, as we saw earlier, static types can sometimes eliminate the need to handle complex errors, and this, in turn, greatly reduces code clutter.

It is difficult to say whether verbosity is a real argument against types, but it is worth keeping it in mind.

Disadvantage number 3: It takes time to achieve skill in the use of types


It takes a lot of time and practice to learn the best way to choose types in the program.Moreover, the development of a good feeling for what should be monitored statically, and what is best left in a dynamic form, also requires a careful approach, practice and experience.

For example, one approach is to encode critical business logic with static types, but leave short-term or unimportant fragments of logic dynamic to avoid unnecessary complexity.

Understanding the difference is difficult, especially if the less experienced developer has to make decisions on the fly.

Disadvantage # 4: Static types can delay fast development.


As I mentioned earlier, I tripped slightly on types when I studied Elm — especially when I added code or made changes to it. Constantly distracted by compiler errors, it is difficult to do work and feel progress.

The argument here is that because of checking static types, a programmer may lose concentration too often - and you know, concentration is a key factor in writing a good program.

It's not just that. Static type controllers are also not always perfect. Sometimes there is a situation when you know what to do, and type checking interferes and interferes.

I'm sure I missed some other flaws, but these are the most important for me.

Need to use static types in javascript or not?




The first programming languages ​​I studied were JavaScript and Python, both languages ​​with dynamic typing.

But mastering static types has added a new dimension to how I think about programming. For example, although I considered the constant compiler error messages in Elm to be overwhelming at first, then the type definition and “compiler favoring” became second nature and actually improved my programming skills. In addition, there is nothing more liberating than a smart robot that tells me that I'm doing something wrong and how to fix it.

Yes, there are inevitable compromises of static types, such as excessive verbosity and the need to spend time studying them. But types add security and correctness to programs, which eliminates the significance of these “flaws” for me personally.

Dynamic types seem faster and simpler, but they can fail when you actually run a program in action. At the same time, you can talk to any Java developer who deals with more complex parameterized types - and he will tell you how much he hates them.

Ultimately, there is no universal solution. Personally, I prefer to use static types under the following conditions:

  1. The program is critical to your business.
  2. The program is likely to refactor according to new needs.
  3. The program is complex and has many moving parts.
  4. The program is supported by a large group of developers who need to quickly and accurately understand the code.

On the other hand, I would refuse static types in the following conditions:

  1. The code is short-lived and is not critical.
  2. You make a prototype and try to move as fast as possible.
  3. The program is small and / or simple.
  4. You are the only developer.

The advantage of developing in JavaScript these days is that, thanks to tools like Flow and TypeScript, we finally have the choice of using static types or good old JavaScript.

Conclusion


I hope these articles have helped you understand the importance of types, how to use them and, most importantly, * when * to use them.

The ability to switch between dynamic and static types is a powerful tool for the JavaScript community, and an exciting one :)

About the author: Preethi Kasireddy, co-founder and lead engineer of Sapien AI, California

Source: https://habr.com/ru/post/326394/


All Articles