This article discusses how to use strongly typed languages in JavaScript code. These techniques not only reduce code errors, but also reduce the amount of code. Although this article takes JavaScript as an example, these tips also apply to most weak-type languages.
Key points:
JavaScript Type System
First, let’s quickly review how the JavaScript data type system works. JavaScript divides its values into two categories:
var a = []; var b = a; a.push('Hello');
When we change a, the variable b also changes because they all refer to the same array. This is how all reference types work. JavaScript does not enforce any type, which means that any variable can save any data type at any time. The rest of this article discusses the drawbacks of this approach and how to apply simple tricks in enforcing types to write better JavaScript code.
Introduce a consistent type rule
The consistent type rule is theoretically simple: all values should have only one type. Strongly typed languages enforce this rule at the compiler level, and they do not allow you to mix and match types at will. Weak types give us a lot of freedom. A common example is concatenating numbers into strings. You don't need to perform as tedious type conversion as in languages like C. Don't worry, I won't tell you to give up all the conveniences. Consistent type rules just require you to pay attention to how variables and functions behave, so your code will be improved.
First, let's see how the rule is applied to a variable. It's very simple: your variable should always have only one type.
var a = []; var b = a; a.push('Hello');
The above example shows the problem. This rule requires us to pretend that the last line of code in this example throws an error because when we first define the variable text, we give it a value of the string type, and now we are assigning a number to it. Consistent type rules mean we do not allow changing the type of the variable in this way. It is easier to infer your code when your variables are consistent. It especially helps with longer functions where it is easy to ignore the source of variables. When working in a codebase that doesn't follow this rule, I accidentally caused a lot of errors because I saw a variable declared and then assumed it would stay the same type - because let's face it, it makes sense , isn't it? There is usually no reason to assign different types to the same variable.
The same rules apply here as well. The parameters of the function should also be consistent. An example of error:
var text = 'Hello types'; // 错误!不要这样做! text = 1;
What's the problem here? It is generally believed that branching logic based on type checking is not a good practice. There are some exceptions to this, but usually a better option is to use polymorphism. You should try to make sure that the function parameter also has only one type. If you forget to consider different types, it reduces the likelihood of problems and makes the code simpler because you don't have to write code to handle all the different types of cases. Better ways to write sum function are as follows:
function sum(a, b) { if (typeof a === 'string') { a = 1; } return a + b; }
Then you handle type checking in the calling code instead of in the function. As can be seen from the above, this function is much simpler now. Even if we have to move the type checking elsewhere, the earlier we execute them in the code, the better it will work. We will discuss the use of type checking and typeof later in this article, including how type checking can be easily cascading if used incorrectly.
This is related to the other two: your function should always return value of the same type. We can give an example of AngularJS here. AngularJS provides a function that converts text to lowercase, called angular.lowercase. There is also a standard function, String.prototype.toLowerCase. We can compare their behavior to better understand this part of the rule:
function sum(a, b) { return a + b; }
Variable a will contain what you expect: "hello types". But what will b include? Will it be an empty string? Will the function throw an exception? Or maybe it's just null? In this case, the value of b is null. Note that it is difficult to guess immediately what the result is – we have three possible outcomes from the beginning. For Angular functions, for non-string values, it will always return the input. Now, let's see how built-in functions behave:
var a = []; var b = a; a.push('Hello');
The result of the first call is the same, but the second call throws an exception. Built-in functions follow consistent type rules and do not allow incorrect parameter types. The return value is always a string. So we can say that built-in functions are better, but you might be wondering exactly how to do this? Let's consider typical use cases for such functions. We use it at some point in our code to convert the string to lowercase. As often happens in JavaScript code, we cannot 100% determine whether our input is always a string. That's OK, because we are good programmers and we assume that our code is not wrong. What happens if we use AngularJS functions that do not comply with these rules? Non-string values pass through it without any problem. It may pass several functions, and we may even send it through the XMLHttpRequest call. Now the wrong value is in our server and eventually goes to the database. You can see what I mean, right? If we use built-in functions that comply with rules, we will immediately find the error at that time. Whenever you write a function, make sure that the types it returns are consistent. A bad example is shown below:
var text = 'Hello types'; // 错误!不要这样做! text = 1;
Similarly, like variables and parameters, if we have such a function, we cannot make assumptions about its behavior. We will need to use if to check the type that returns value. We may forget it at some point and then another error will appear in our hands. We can rewrite it in a number of ways, here is a solution to this problem:
function sum(a, b) { if (typeof a === 'string') { a = 1; } return a + b; }
This time we make sure that all paths return a string. It is now easier to infer the results of the function.
null and undefined are special
So far, we've actually only discussed the original genre. You should follow the same rules when it comes to objects and arrays, but you need to be aware of two special cases. When processing reference types, sometimes it is necessary to indicate that there is no value. A good example is document.getElementById. If the matching element is not found, it returns null. This is why we consider null as sharing a type with any object or array, but only those objects or arrays. You should avoid returning null from functions that may return original values such as Number. undefined can also be considered a referenced "valueless". For most purposes it can be considered equal to null, but null is preferable due to its semantics in other object-oriented languages.
When using arrays, you should also consider that empty arrays are usually better than null . Although arrays are reference types, you can use null with them, it usually makes more sense to return an empty array. Let's look at the following example:
var a = []; var b = a; a.push('Hello');
This is probably one of the most common uses of arrays. You get an array from the function and iterate over it to perform other operations. What happens to the above code if getListOfItems returns null when there is no project? It throws an error because null has no length (or any other attribute). When you consider using arrays like this, or even list.forEach or list.map, you can see that returning an empty array when there is no value is usually a good idea.
Type checking and type conversion
Let's learn more about type checking and type conversion in more detail. When should you do type checking? When should you do type conversion?
type conversion should be to make sure your value is of the correct type. The value should be a number rather than a string, and so on. The second goal should be that you only need to convert the value once. The best place to perform type conversion is at the source. For example, if you get data from the server, you should do any necessary type conversion in the function that processes the received data. Parsing data from the DOM is a very common place where errors start to occur. Suppose you have a text box containing numbers and you want to read it. Or, it might just be an attribute in some HTML element, and it doesn't even have to be user input.
var text = 'Hello types'; // 错误!不要这样做! text = 1;
Since the values you can get from the DOM are usually strings, it is important to type convert when reading them. To some extent, you can think of it as the "edge" of the module. The data goes into your JavaScript module by reading its functions, so it must convert the data to the correct format. By doing type conversion at the edge of the module, we make sure that it doesn't have to be processed internally. This greatly reduces the possibility of implicit type casting causing errors. It also allows us to write less code because we do not allow wrong values to enter the module from the edge.
function sum(a, b) { if (typeof a === 'string') { a = 1; } return a + b; }
You should only use typeof for validation, not based on type branch logic. There are some exceptions to this, but it's a good rule of thumb. Let's look at two examples:
function sum(a, b) { return a + b; }
This is an example of using typeof for validation. We make sure that the parameters passed to the function are of the correct type. However, the following example shows what the branch logic is based on type.
var a = []; var b = a; a.push('Hello');
Don't do this. While it may be necessary to do so sometimes, this is often a sign of poor design. If you find yourself executing this logic frequently, you should probably convert value to the correct type early in your code. If you end up using a lot of typeof in your code, this may mean that you may need to convert the value you want to compare. Type checks often spread, which is usually a sign of poorly designed types. As mentioned earlier, you should try type conversion at the module edge, as it allows you to avoid typeof cascade. If you convert as early as possible, no function you call later does not have to do type checking or type conversion. This also applies to objects: If you find yourself using instanceof for a lot of checks or checking for properties on the object, it means you should probably construct the data in a different way. The same rules as typeof also apply to instanceof: you should try to avoid it, as it may be a bad design sign. However, there is one situation that is inevitable:
var text = 'Hello types'; // 错误!不要这样做! text = 1;
If your code needs to handle exception types specific, instanceof is usually a good choice, as JavaScript catch does not allow distinction by type like some other languages. In most other cases, you should try to avoid instanceof.
Conclusion
As we have discovered, the weak types of JavaScript bring us great freedom, but we must also be cautious. Otherwise, we will end up in a genre chaos with no meaning. By making sure our code follows consistent type rules, we can avoid a lot of trouble. It is easier to infer our code when we know the type. We don't have to build many type checks in our code to prevent errors. This seems difficult if you are not using a strongly typed language, but it pays a lot when you need to debug or maintain your code. For more information on this topic, I recommend you check out TypeScript. It is a JavaScript-like language, but it adds stronger type semantics to the language. It also has a compiler that spits out an error when you try to do some stupid things like mixing and matching types.
FAQs about Strongly Typed Languages in JavaScript (FAQ)
Strongly typed language refers to languages in which variables are bound to a specific data type. Any operation that is inconsistent with that type will cause an error. Examples include Java and C. On the other hand, weakly typed languages like JavaScript allow variables to hold any type of data and automatically type conversions if necessary. If handled improperly, this flexibility can lead to unexpected results.
JavaScript itself is a weakly typed language, but you can enforce strong type using TypeScript, a statically typed superset of JavaScript. TypeScript adds static types to JavaScript, allowing type checking at compile time. This helps to detect errors early in the development process. "Strict Pattern" is another method in JavaScript that makes the language behave more like a strongly typed language by throwing errors for insecure actions.
Strongly typed languages offer some benefits. They can help catch errors at compile time rather than runtime, which can save a lot of debugging time. They also make the code more self-documentary, because the data types of variables clearly indicate how they are used. Furthermore, they can make the code more predictable and easier to infer because they prevent unexpected type conversions.
While JavaScript is not a strongly typed language by default, you can enforce strong typed using tools such as TypeScript or Flow. These tools add static types to JavaScript, allowing type checking at compile time. This helps to detect errors early in the development process. However, it is important to note that these tools do not change the underlying nature of JavaScript; they simply provide a layer of type safety on top of it.
TypeScript is a JavaScript statically typed superset developed by Microsoft. It adds static types to JavaScript, allowing type checking at compile time. This helps to detect errors early in the development process. TypeScript code is converted to JavaScript, which means it can run anywhere JavaScript runs. It is fully compatible with JavaScript and can use all the features of JavaScript.
While strongly typed languages offer many benefits, they also have some disadvantages. They can be more verbose and require more code to complete the same tasks as weak-type languages. They also require a compilation step, which may slow down the development process. Furthermore, they may be less flexible and are more difficult to use for certain tasks, such as handling dynamic data.
"Strict Pattern" is a feature in JavaScript that makes the language behave more like a strongly typed language. It throws errors for unsafe actions, such as assigning values to read-only properties or using variables before declaring them. This helps to detect errors early in the development process. To enable "strict mode", just add a line "use strict;" at the top of a JavaScript file or function.
Type coercion is a feature in JavaScript where the language automatically converts one data type to another when necessary. For example, if you try to add numbers and strings, JavaScript will convert the numbers to strings before performing addition. While this is convenient, it can also lead to unexpected results if handled improperly.
One way to avoid type coercion in JavaScript is to use "strict mode", which throws errors for unsafe actions. Another way is to use the "===" operator instead of the "==" operator for comparison, because the former does not perform type coercion. Additionally, you can add static types to JavaScript using tools like TypeScript or Flow, which helps catch type-related errors at compile time.
The use of strongly typed languages in JavaScript may increase in the future because they provide many benefits such as catching errors early and making the code more predictable. Tools such as TypeScript and Flow are becoming more popular, and new tools and features are being developed to make JavaScript more type-safe. However, JavaScript's flexibility and dynamicity will continue to be the first choice for many developers.
The above is the detailed content of Borrowing Techniques from Strongly Typed Languages in JS. For more information, please follow other related articles on the PHP Chinese website!