Making TypeScript stricter. Yandex report

How to make TypeScript a strict but fair companion who will protect you from nasty bugs and give you more confidence in your code? Alexey Veselovsky veselovskiyaiconsidered several features of the TS configuration that turn a blind eye to unforgivable liberties. The report outlines those things that are best avoided and those with which you need to be extremely careful. You will learn about the wonderful io-ts library - it allows you to easily detect and even prevent data entering the code that can cause errors in perfectly written places.



- Hello everyone, my name is Lesha, I'm a frontend developer. Let's start. I will tell you a little about myself and the project in which I work. Flow is learning English from Yandex.Practicum. The release took place in April this year. The front was written directly in TypeScript, before that there was no code.







A little about my experience. In some distant year, I started programming. A year in 2013, he began to work.







Almost immediately I realized that I was much more interested in the front, but I had experience with statically typed languages. I started using JavaScript and this static typing was not there. It seemed to me convenient, I liked it.



On a project change, I got to work using TypeScript. I'll tell you about the pros that I realized by switching to TypeScript. Easier to understand the project. We have a description of the data types that are used in the project and conversions between them.







It is safer to make changes to the code: in case of some changes in the backend or just some part of the code, TypeScript will highlight the places where errors appeared.



There is less concern about types. When we create new functionality, we immediately set the types with which the functions work, and we can less worry that we will receive different data.



There is no fear that null or undefined will come, we do not need to be paranoid, insert unnecessary if and similar constructs.







At the beginning of this year, I moved to Flow. TypeScript is also used here, but I didn't recognize it a bit. Why? He was too kind to me, a quarter of client errors were related to null and undefined. I started to figure out what was going on and found one line in the configuration that changed all the behavior of TypeScript







This is the inclusion of strict. It was not there, but it needed to be turned on to improve verification.



TypeScript: strict



What is strict? What does it consist of?







This is a set of flags that can be turned on individually, but in my opinion they are all very useful. noImplicitAny - before enabling this flag, we can declare, for example, functions whose parameters will be implicit, such as any. If we enable this flag, then we must add typing in places where TypeScript cannot calculate the type from the context.



That is, in the second case, we must add typing, since there is no context as such. In the third case, where we have a map, we can not add typing for a, because it is clear from the context that there will be a number type.







noImplicitThis. TypeScript obliges us to type this when there is no context. When the context is, that is, it is an object or a class, we do not need to do this.







alwaysStrict. Adds “use strict” to every file. But it does affect how JavaScript executes our code. (...)







strictBindCallApply. For some reason, before enabling this option, TypeScript does not check bind, apply and call for types. After turning it on, he checks them and does not allow us to do such nasty things.







strictNullChecks is, in my opinion, the most needed check. It obliges us to indicate in the typing the places where null or undefined can come. Before inclusion, we can pass null or undefined where it is not explicitly specified, and, accordingly, get an error. After that, the control will be much better.







Next, strictFunctionTypes. The situation here is a little more complicated. Let's imagine we have three functions. One works with animals, another with dogs, and one with cats. A dog and a cat are animals. That is, it will be erroneous to work with a dog in the same way as with a cat, because they are different. It will work correctly with a dog just like with an animal.



The third option is when we try to work with any animal like a dog. For some reason, it is initially allowed in TypeScript, but if you enable this option, it will be invalid and certain checks will be performed.







Next, strictPropertyInitialization. This is for classes. It obliges us to set initial values ​​either when declaring a property or in a constructor. Sometimes there are times when you need to get around this rule. You can use an exclamation mark, but again, this obliges us to be a little more careful.



So, I figured out that we need to enable strict. I try to turn it on, and a lot of errors pop up. Therefore, it was decided to use a transitional configuration to strict. We set strict in three steps.







The first stage: we add “strict”: true to tsconfig, and, accordingly, our development environment prompts us for places with an error, which is caused by the inclusion of strict.



But for webpack, we create a special tsconfig, which strict will be false, and use this when building. That is, nothing breaks during assembly, but in our editor we see these errors. And we can fix them right away. Then we turn from time to time to the second stage, this is a fix. We build our project with the usual tsconfig. We correct some of the mistakes that have come out, and repeat all this in our free time.



By such actions, we have so far reduced the number of our errors from 400 to 200. We are looking forward to moving on to the third stage - removing webpackTsConfig and using tsconfig when building, but with strict enabled.



TypeScript:



You can talk a little about the small subtleties of TypeScript that are not covered by strict, but they are difficult to formalize correctly.







Let's start with the exclamation mark operator. What does it allow you to do? In this case, refer to a field that can be undefined, as if it cannot be undefined. It makes sense in strict mode, when we try to access a field, explicitly saying: I'm sure that it is definitely not null or undefined. But this is bad, because if it suddenly turns out to be null or undefined, then we naturally get an error at runtime.



ESLint will help us to avoid such things, it will simply prohibit us. We did it. How do I fix the previous example now?



Suppose we have such a situation.







There is an element, it can be of type link or span. With our head we understand that span is only text, and link is text and link.



(picture)



But we forgot to tell the TypeScript language, so in the getItemHtml function a situation arises that in the case of link we have to say: href is not optional, it will definitely be. This is also a potential place for error. How to fix it?







The first option is to correct the typing, that is, to explicitly indicate to TypeScript that an href is required for a link, and an optional for span.







And the exclamation mark will not be needed here.







Second correction option. Suppose the Item type is not described by us and we cannot just take and restrict it. Then we can rewrite it in a similar way.







Please note: the check just appeared. Next comes the logging that the programmer did not expect this value when writing this code, so in the future we will see this error and take appropriate action.



Next, we are trying to somehow render our Item. Here you can simply give the user an error. But if this is some insignificant data, then you can make a stub like here.



as





Further. There is also an as operator. What does it allow you to do?







It allows you to say - I know better, there is such and such a type - and also lead yourself to a mistake.



Arrays



The methods of struggle are the same. What you need to be a little more careful with is arrays. TypeScript is not a panacea, it will not check some points. For example, we can refer to a nonexistent array element. In this case, we will take the first element of the array and get an error in this code. How can we fix this?







Again, there are two ways. The first way is typing. We say that we have the first element, and fearlessly refer to this element. Or we will check, we will log, if something is suddenly wrong, if we explicitly expect a non-empty array.



Objects



It's the same with objects. We can declare an object, which can have any number of properties, and also get an undefined error.







Again, you can make explicit instructions which properties are required, or just check.



any



Now the obvious thing is any.







It allows you to refer to any property of an object as if there were no typing at all. In this case, we can do whatever we want with x. And again shoot yourself in the foot, get mistakes.



Again, it is better to explicitly disallow this with ESLint. But there are situations when it appears on its own.







For example, in this case JSON.parse yields just this type any. What can be done?







You can simply say: I don’t believe you, let’s better say that I don’t know what it is, and I’ll live with it on. How to live with it? Here's a hypothetical example.







There is a user, the user has a required name and an optional e-mail.







We are writing the parseUser function. It takes a JSON string and returns our object to us. Now we begin to check all this. First, we see the line with parse and unknown familiar to us from the previous slide. Next, we start checking.







If it is not an object or is null, throw an error.







Further, if there is no required name property or it is not a string, we throw an error. Here is the continuation of the code.







We start to form User, since all the required fields have already been collected.







Next, we check if there is an email field. If it is, then we check its type and, if the type does not match, we throw an error. If there is no email, then we do not send anything and return the result. Everything is fine. But you need to write a lot for the simplest type.







And it takes a lot of checks



We need a lot of validation because a typical JSON request looks like this.







Without further ado, this is just fetch and json (). The conversion from any to SomeRequestResponse appears in return. This also needs to be fought. You can do it in the previous way, or you can do it a little differently.



io-ts



It's the same under the hood: we use a special library for type checking. In this case, it is io-ts. Here's a simple example of how to work with it.







Let's take the previous user type and write it within the library we are using. Yes, typing is a little more complicated here, but two conditions must be met simultaneously. It must be an object with a required name field and an object with an optional email field. How can we check all this?







Let's write the same parseUser. In this case, we are using the User.decode method. We pass the already paired object there, it returns the result to us. Perhaps in an unusual format. An object of type Either, it can be in two states. The first is right. This usually means that everything went well. left says it didn't go very well. Both of these states have properties that allow us to learn more. If successful, this is the result of the execution, if it fails, it is an error.



We check if our results are in the left state. If they are, we say that an error has occurred. Then, if everything is fine, we simply return the result.



Displaying errors







About displaying errors. You can improve it a little. We'll use io-ts-reporters for this. It is a library written by the same author as io-ts. It allows the error to be presented beautifully. What she does? We changed the code here where the duck is. It takes the result and returns an array of strings. We just join it in one line and display it. What do we get in the end?







Suppose we are passing null to a JSON string.







It will give two errors. This is due to the subtlety of the implementation, because we did intersection. The errors are clear enough. Both of them say that we expected an object but got null. It's just that it will give an error separately for each of these conditions.







Next, let's try to pass an empty array there. It will be the same.







He will simply tell us: I also expected an object, but received an empty array.







So, we continue to see what will happen if we start transmitting incorrect data. For example, let's pass an empty object.







Now it will give one error about the fact that we do not have the required name field. He expected the name field to be of type string, but ends up with undefined. It is also easy to understand from this error what happened.







Next, we will try to pass an incorrect type there. We also get an error, about the same as in the previous example.







But here he clearly writes to us the meaning that we conveyed.







What else can io-ts do? It allows you to get a TypeScript type. That is, we add this line. By simply adding typeof, also typeof, we get a TypeScript type that we can further use in our application. Conveniently.







What else can this library do? Convert types. Let's say we are making a request to the server. The server sends dates in unix time format. And there is a special library, again from the creator of the io-ts library: io-ts-types. There are transformations that were originally written, and tools to make those transformations easier to write. We add a date field: it comes from the server as a number, and we end up receiving it as a Date object.



Let's describe the type



Let's see what's inside this library and try to describe the simplest type.







First, let's see how it is generally described. It is described in the same way, rather complicated, given that it is also needed for transformations. Aside from the server to the client, if we consider the interaction with the server, and the reverse transformation, from the client to the server.



Let's simplify our task a little. We'll just write the type that checks. In this case, let's figure out what these fields mean. name - type name.







It is required to display errors. As we saw in the previous examples, errors somehow spell out the type name. You can specify it here.



Next, there is the validate function. It takes - let's say, from the server - the value unknown; takes a context to correctly display the error; and returns an Either object in two states - either an error or a validated value.



There are two more functions: is and encode. They are used to reverse transform them, but let's not touch them for now.







How can the simplest string type be represented? We set the name to string and check that it is a string. With a direct conversion, this will not be necessary, but formally we write it. And then we just do typeof to check. If successful, we return the result success, and as a result of an error, failure. The context is also added so that the error is displayed correctly. And we just return the same thing, because there is no reverse transformation.



On practice



What's in practice? Why did we decide to check the data that comes from the server at all?







At a minimum, there is JSON in the database. We, of course, believe that he will be well-run and that he will be checked at some points. But the format may change a little, we must not break the frontend or immediately find out about errors in order to take retaliatory actions.



We have Python on the server without explicit typing. With this, too, sometimes there can be small problems. And in order not to break down, we can simply check and additionally secure ourselves, just in case.



There is no clear documentation on server responses. Probably, the server is more worried about what will come to it than about what it will give. Yes, this is more our problem - not to break.







What did we find? We have already started to use it a little bit. Found that the server gives us an empty object instead of an empty array. I just looked through the code - written to return an empty object.



Further - the absence of some fields. We thought they were mandatory, but they turn out to be optional.



A nullable field was simply missing in some cases. That is, an optional field can be presented in two ways: either when we simply do not pass it, or when we pass null. It also did not always come to us correctly. In order not to catch errors in the middle of our code, we can catch this just at requests.







What do we have now? We have already checked a lot of responses from the server and logged if we don't like something. Then we analyze this and set tasks: either for changing the typing on our frontend, or edits on the backend. Now we do not change the data that comes from the server: if null came instead of a string, we do not change it, for example, to an empty string.



Our plans are to check and log, but correct if there is an error. If we receive incorrect data, we will correct this value so that users can display at least something instead of falling inside our code.







Small results. We turn on strict so that TypeScript will help us more, exclude as, any, and the exclamation mark. We will be more careful with arrays and objects in TypeScript, and also check all external data. By the way, these are not only servers. You can also check localStorage, messages that come in events. For example postMessage.



Thanks for attention.



All Articles