Effective TypeScript: 62 Ways to Improve Your Code

imageHello Habitants! Dan Vanderkam's book will be most useful for those who already have experience with JavaScript and TypeScript. The purpose of this book is not to educate readers to use the tools, but to help them improve their professionalism. After reading it, you will gain a better understanding of how TypeScript components work, avoid many pitfalls and pitfalls, and develop your skills. While the reference guide will show you five different ways of using the language to accomplish the same task, an “effective” book will explain which one is better and why.



Book structure



The book is a collection of short essays (rules). The rules are grouped into thematic sections (chapters), which can be accessed autonomously, depending on the question of interest.



Each rule heading contains a tip, so check out the table of contents. If, for example, you are writing documentation and are in doubt about whether to write type information, refer to the table of contents and rule 30 (“Do not repeat type information in the documentation”).

Almost all of the conclusions in the book are demonstrated using code examples. I think you, like me, tend to read technical books by looking at examples and only passing through the text part. Of course, I hope you read the explanations carefully, but I've covered the main points in the examples.



After reading each tip, you can understand exactly how and why it will help you use TypeScript more effectively. You will also understand if it turns out to be unusable in some case. I remember an example given by Scott Myers, author of Effective C ++: missile software developers might have neglected advice on preventing resource leaks because their programs were destroyed when the missile hit the target. I am not aware of the existence of rockets with a control system written in JavaScript, but such software is on the James Webb telescope. So be careful.



Each rule ends with a "Must Remember" block. Having a quick look at it, you can get a general idea of ​​the material and highlight the main thing. But I highly recommend reading the entire rule.



An excerpt. RULE 4. Get used to structural typing



JavaScript is unintentionally duck-typed: if you pass a value with the correct properties to a function, it doesn't care how you got that value. She just uses it. TypeScript models this behavior, which sometimes leads to unexpected results, as the validator's understanding of types may be broader than yours. Developing the skill of structured typing will allow you to better feel where there really are errors and write more reliable code.



For example, you are working with a physics library and you have a 2D vector type:



interface Vector2D {
   x: number;
   y: number;
}


You write a function to calculate its length:



function calculateLength(v: Vector2D) {
   return Math.sqrt(v.x * v.x + v.y * v.y);
}


and enter the definition of the named vector:



interface NamedVector {
   name: string;
   x: number;
   y: number;
}


The calculateLength function will work with the NamedVector since it contains the x and y properties, which are number. TypeScript understands this:



const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v); // ok,   5.


The interesting thing is that you didn't declare a relationship between Vector2D and NamedVector. You also didn't have to write an alternative calculateLength execution for the NamedVector. The TypeScript type system simulates JavaScript's runtime behavior (rule 1), which allowed the NamedVector to call calculateLength based on its structure being comparable to Vector2D. Hence the expression "structural typing".



But it can also lead to problems. Let's say you add a 3D vector type:



interface Vector3D {
   x: number;
   y: number;
   z: number;
}


and write a function to normalize vectors (make their length equal to 1):



function normalize(v: Vector3D) {
   const length = calculateLength(v);
   return {
      x: v.x / length,
      y: v.y / length,
      z: v.z / length,
   };
}


If you call this function, you will most likely get more than one length:



> normalize({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 1 }


What went wrong and why didn't TypeScript report a bug?



The bug is that calculateLength works with 2D vectors, while normalize works with 3D. Therefore, the z component is ignored during normalization.



It might seem odd that the type checker didn't catch this. Why is it allowed to call calculateLength on a 3D vector, even though its type works with 2D vectors?

What worked well with named has backfired here. Calling calculateLength on the {x, y, z} object will not throw an error. Therefore, the type checking module does not complain, which ultimately results in a bug. If you would like the error to be detected in such a case, refer to rule 37.



When writing functions, it's easy to imagine that they will be called by the properties you declared, and nothing else. This is called a "sealed" or "exact" type and cannot be applied in the TypeScript type system. Like it or not, the types are open here.



Sometimes this leads to surprises:



function calculateLengthL1(v: Vector3D) {
   let length = 0;
   for (const axis of Object.keys(v)) {
      const coord = v[axis];
                       // ~~~~~~~     "any",  
                       //  "string"    
                       //    "Vector3D"
      length += Math.abs(coord);
   }
   return length;
}


Why is this a mistake? Since axis is one of the v keys from Vector3D, it must be x, y, or z. And according to the original Vector3D declaration, they are all numbers. Therefore, shouldn't the coord type also be number?



This is not a false error. We know that Vector3D is strictly defined and has no other properties. Although he could:



const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway'};
calculateLengthL1(vec3D); // ok,  NaN


Since v could probably have any properties, axis is of type string. There is no reason for TypeScript to think of v [axis] as just a number. When iterating over objects, it can be difficult to achieve correct typing. We'll come back to this topic in Rule 54, but for now we do not use loops:



function calculateLengthL1(v: Vector3D) {
   return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}


Structural typing can also cause surprises in classes that are compared for possible property assignments:



class C {
   foo: string;
   constructor(foo: string) {
       this.foo = foo;
   }
}

const c = new C('instance of C');
const d: C = { foo: 'object literal' }; // ok!


Why can d be assigned to C? It has a property foo which is string. It also has a constructor (from Object.prototype) that can be called with an argument (although it is usually called without it). So the structures are the same. This can lead to surprises if you have logic in the C constructor and write a function to invoke it. This is a significant difference from languages ​​like C ++ or Java, where declarations of a C type parameter ensure that it belongs to C or a subclass of it.



Structural typing helps a lot when writing tests. Let's say you have a function that executes a database query and processes the result.



interface Author {
   first: string;
   last: string;
}
function getAuthors(database: PostgresDB): Author[] {
   const authorRows = database.runQuery(`SELECT FIRST, LAST FROM
                                 AUTHORS`);
   return authorRows.map(row => ({first: row[0], last: row[1]}));
}


To test it, you could create a PostgresDB mock. However, a better solution would be to use structured typing and define a narrower interface:



interface DB {
   runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
   const authorRows = database.runQuery(`SELECT FIRST, LAST FROM
                                 AUTHORS`);
   return authorRows.map(row => ({first: row[0], last: row[1]}));
}


You can still pass postgresDB to the getAuthors function in the output, since it has a runQuery method. Structural typing does not oblige PostgresDB to report that it is executing a DB. TypeScript will figure it out for you.



When writing tests, you can also pass in a simpler object:



test('getAuthors', () => {
   const authors = getAuthors({
      runQuery(sql: string) {
         return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
      }
   });
   expect(authors).toEqual([
      {first: 'Toni', last: 'Morrison'},
      {first: 'Maya', last: 'Angelou'}
   ]);
});


TypeScript will determine that the test DB conforms to the interface. At the same time, your tests do not need any information about the output database at all: no mock libraries are required. By introducing abstraction (DB), we freed the logic from execution details (PostgresDB).



Another advantage of structural typing is that it can clearly break dependencies between libraries. For more information on this topic, see rule 51.



PLEASE REMEMBER



JavaScript uses duck typing, and TypeScript models it using structured typing. As a result, the values ​​assigned to your interfaces may have properties not specified in the declared types. Types in TypeScript are not sealed.



Keep in mind that classes also obey structural typing rules. Therefore, you may end up with a different class sample than expected.



Use structured typing to make it easier to test items.


RULE 5. Restrict the use of types any



The type system in TypeScript is gradual and selective. Graduality is manifested in the ability to add types to the code step by step, and selectivity in the ability to disable the type checking module when you need it. The key to control in this case is the any type:



   let age: number;
   age = '12';
// ~~~  '"12"'       'number'.
   age = '12' as any; // ok


The entitlement module, indicating an error, but it can be avoided by simply adding as any. When you start working with TypeScript, it becomes tempting to use any types or as any assertions if you don't understand the error, don't trust the validator, or don't want to spend time spelling out the types. But remember that any negates many of the benefits of TypeScript, namely:



Reduces code safety



In the example above, according to the declared type, age is number. But any allowed a string to be assigned to it. The validator will assume that this is a number (which is what you declared), which will lead to confusion.



age += 1; // ok.    age "121".


Allows you to violate conditions



When creating a function, you set a condition that, having received a certain data type from a call, it will produce the corresponding type in output, which is violated like this:



function calculateAge(birthDate: Date): number {
   // ...
}

let birthDate: any = '1990-01-19';
calculateAge(birthDate); // ok


The birthDate parameter must be Date, not string. The any type allowed the condition related to calculateAge to be violated. This can be especially problematic because JavaScript tends to do implicit type conversions. Because of this, string will fail in some cases where number is supposed to, but will inevitably fail elsewhere.



Eliminates support for a language service



When a character is assigned a type, TypeScript language services are able to provide appropriate auto-substitution and contextual documentation (Figure 1.3).



image


However, by assigning the type any to characters, you have to do everything yourself (Figure 1.4).



image


And renaming too. If you have a Person type and functions to format the name:



interface Person {
   first: string;
   last: string;
}

const formatName = (p: Person) => `${p.first} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;


then you can highlight first in the editor and select the Rename Symbol to rename it to firstName (Figure 1.5 and Figure 1.6).



This will change the formatName function, but not in the case of any:



interface Person {
   first: string;
   last: string;
}

const formatName = (p: Person) => `${p.firstName} ${p.last}`;
const formatNameAny = (p: any) => `${p.first} ${p.last}`;




image




»More details about the book can be found on the publisher's website

» Table of Contents

» Excerpt



For Habitants a 25% discount on coupon - TypeScript



Upon payment for the paper version of the book, an e-book is sent to the e-mail.



All Articles