More and more projects and teams are using TypeScript. However, just applying TypeScript and squeezing the most out of it is very different things.
I present to you a list of high-level best practices for using TypeScript that will help you get the most out of this language.
Types is a contract. What does it mean? When you implement a function, its type becomes a promise to other developers (or to yourself in the future!) That, when called, this function will return a certain type of value.
In the following example, the type of the getUser
function ensures that it returns an object that always has two properties: name
and age
.
interface User { name: string; age: number; } function getUser(id: number): User { /* ... */ }
TypeScript is a very flexible language. It has many compromises designed to facilitate the introduction of the language. For example, it allows you to implement the getUser
function like this:
function getUser(id: number): User { return { age: 12 } as User; }
Do not do that! It's a lie. By creating such code, you are LIES to other developers (who will use your function in their functions). They expect that the object returned by getUser
will always have some kind of name
field. But he is not there! Further, what happens when your colleague writes getUser(1).name.toString()
? You know very well that ...
Here, of course, the lie seems obvious. However, working with a large code base, you will often find yourself in situations where the value you want to return (or pass) almost matches the expected type. It takes time and effort to find the cause of the type mismatch , and you are in a hurry ... so you decide to use type casting.
However, by doing this, you are breaking the sacred contract . It is ALWAYS better to take the time and understand why the types do not match than to use type casting. It is very likely that some runtime bug is hiding beneath the surface.
Do not lie. Obey your contracts.
Types are documentation. When documenting a function, don't you want to convey as much information as possible?
// function getUser(id) { /* ... */ } // : name age function getUser(id) { /* ... */ } // id id , // : name age. // undefined. function getUser(id) { /* ... */ }
What comment for getUser
function getUser
you like more? The more you know that a function returns, the better. For example, knowing that it can return undefined
, you can write an if
block to check whether the object that the function returned is defined before requesting the properties of this object.
Exactly the same thing with types: the more accurately the type is described, the more information it conveys.
function getUserType(id: number): string { /* ... */ } function getUserType(id: number): 'standard' | 'premium' | 'admin' { /* ... */ }
The second version of the getUserType
function getUserType
much more informative, and therefore the caller is in a much more convenient situation. It is easier to process the value if you probably know (contracts, remember?) That it will be one of the three given lines, and not just any line. To start with what you know for sure - a value cannot be an empty string.
Let's consider a more real example. The State
type describes the state of the component that requests some data from the backend. Is this type accurate?
interface State { isLoading: boolean; data?: string[]; errorMessage?: string; }
A client using this type must handle some unlikely combination of state property values. For example, it is impossible for the data
and errorMessage
properties to be simultaneously defined: the data request may either succeed or fail.
We can make the type much more accurate with the help of discriminated union types :
type State = | { status: 'loading' } | { status: 'successful', data: string[] } | { status: 'failed', errorMessage: string };
Now the client using this type has much more information: he no longer needs to process incorrect combinations of properties.
Be precise. Pass as much information as possible on your types.
Since types are both a contract and documentation, they are great for designing your functions (or methods).
There are many articles on the Internet that advise programmers to think before writing code . I fully share this approach. The temptation to jump directly to the code is great, but this often leads to poor decisions. A little time spent thinking about the implementation always pays off handsomely.
Types are extremely useful in this process. Thinking leads to the creation of signatures of the types of functions associated with the solution of your problem. And that's great, because you focus on what your functions do, instead of thinking about how they do it.
React JS has the concept of a Higher Order Components (HOC). These are functions that extend the given component in some way. For example, you can create a higher-order component withLoadingIndicator
that adds a loading indicator to an existing component.
Let's write a type signature for this function. The function accepts a component input and returns a component too. To represent a component, we can use the React ComponentType
type.
ComponentType
is a generic type that is parameterized by the type of component properties. withLoadingIndicator
accepts the component and returns a new component that displays either the original component or the loading indicator. The decision on what to display is made based on the value of the new logical property - isLoading
. Thus, the returned component needs the same properties as the original, only the new isLoading
property is added.
We will finalize the type. withLoadingIndicator
accepts a component of type ComponentType<P>
, where P
denotes the type of property. withLoadingIndicator
returns a component with advanced properties of type P & { isLoading: boolean }
.
const withLoadingIndicator = <P>(Component: ComponentType<P>) : ComponentType<P & { isLoading: boolean }> => ({ isLoading, ...props }) => { /* ... */ }
Dealing with the types of functions, we were forced to think about what will be on its input and what will be on the output. In other words, we had to design a function . Writing its implementation now is easy.
Start with types. Let types force you to design first, and only after that write an implementation.
The first three commandments require you to pay special attention to types. Fortunately, when solving this problem, you don’t have to do everything yourself - often the TypeScript compiler will let you know when your types are lying or when they are not accurate enough.
You can help the compiler do this even better by including the --strict
flag. This is a meta flag that --noImplicitAny
all strict type checking options: --noImplicitAny
, --noImplicitThis
, --alwaysStrict
, --strictBindCallApply
, --strictNullChecks
, --strictFunctionTypes
and --strictPropertyInitialization
.
What do the flags do? Generally speaking, their inclusion leads to an increase in the number of TypeScript compilation errors. And this is good! More compilation errors - more help from the compiler.
Let's see how turning on the --strictNullChecks
flag helps to detect a false in the code.
function getUser(id: number): User { if (id >= 0) { return { name: 'John', age: 12 }; } else { return undefined; } }
The getUser
type ensures that the function always returns an object of type User
. However, look at the implementation: a function may also return undefined
!
Fortunately, enabling the --strictNullChecks
flag results in a compilation error:
Type 'undefined' is not assignable to type 'User'.
The TypeScript compiler detects falsehoods. To get rid of this error, just honestly tell the whole truth:
function getUser(id: number): User | undefined { /* ... */ }
Accept the rigor of type checking. Let the compiler protect you from errors.
TypeScript is developing at a very fast pace. A new release is released every two months. Each release brings significant language improvements and new features.
It often happens that the new features of the language allow you to define types more accurately and check them more strictly.
For example, in version 2.0 Discriminated Union Types were introduced (I mentioned them in the commandment Be precise ).
Version 3.2 introduced the compiler flag --strictBindCallApply
, which includes the correct typing for the bind
, call
and apply
functions.
Version 3.4 improved type inference in higher-order functions , which made it easier to use exact types when writing code in a functional style.
My position is that getting to know the language features introduced in recent versions of TypeScript is actually worth it. Often this can help you follow the other four commandments from the list.
A good starting point is the official TypeScript Roadmap . It will also be nice to regularly check the TypeScript section in Microsoft Devblog , since all release announcements go there.
Stay up to date with the new features of the language, and let this knowledge work for you.
I hope you find the list helpful. As always and in everything, one should not blindly follow these commandments. But I strongly believe that these rules will make you a better TypeScript developer.
I will be glad to see your thoughts on this subject in the comments.
Did you like this article about TypeScript? I’m sure you will also like this free PDF: 10 TypeScript development errors that make your code unsafe.
Source: https://habr.com/ru/post/461565/
All Articles