Book “Professional TypeScript. Developing Scalable JavaScript Applications "

imageAny programmer working with a dynamically typed language will attest that the task of scaling code is incredibly difficult and requires a large team of engineers. This is why Facebook, Google and Microsoft have come up with static typing for dynamically typed code.



Working with any programming language, we track exceptions and read code line by line to find the problem and how to fix it. TypeScript lets you automate this frustrating part of the development process.



TypeScript, unlike many other typed languages, is application-oriented. It introduces new concepts that allow you to express ideas more succinctly and accurately, and easily build scalable and secure modern applications.



Boris Cherny helps you to understand all the nuances and capabilities of TypeScript, teaches you how to eliminate errors and scale the code.



Book structure



I (the author) have tried to give you a theoretical understanding of how TypeScript works and a fair amount of practical coding advice.



TypeScript is a practical language, so theory and practice mostly complement each other in the book, but the first two chapters mainly cover theory, and towards the end only practice is presented.



We'll cover the basics like the compiler, the type checker, and the types themselves. Next, we discuss their varieties and operators, and then move on to advanced topics such as specifics of the type system, error handling, and asynchronous programming. Finally, I'll show you how to use TypeScript with your favorite frameworks (frontend and backend), migrate an existing JavaScript project to TypeScript, and run a TypeScript application in production.



Most chapters end with a set of exercises. Try them yourself to get a better grasp of the material. The answers to them are available here .



Staged migration from JavaScript to TypeScript



TypeScript was designed with JavaScript interoperability in mind. So while it's not completely painless, migrating to TypeScript is still an enjoyable experience, allowing you to transform your codebase file at a time, get deeper levels of security and commit after commit, and surprise your boss and coworkers how powerful statically typed code can be. ...



At a high level, the codebase should be fully written in TypeScript and strongly typed, and the third-party JavaScript libraries you depend on should be of good quality and have their own strong types. The coding process will be doubled with compile-time error catching and the rich TypeScript autocomplete system. To achieve a successful migration result, it will take a few small steps:



  • Add TSC to the project.
  • Start checking the types of existing JavaScript code.
  • Move JavaScript code to TypeScript file by file.
  • Install type declarations for dependencies. That is, allocate types for dependencies that do not have them, or write type declarations for untyped dependencies and send them back to DefinitelyTyped1.
  • Enable strict mode for the code base.


This process may take a while, but you will immediately see the security and performance gains and other benefits later. Let's take a look at the steps listed.



Step 1: adding TSC



When working with a code base that combines TypeScript and JavaScript, first let TSC compile the JavaScript files along with the TypeScript files in the tsconfig.json settings:



{
     "compilerOptions": {
     "allowJs": true
}


This change alone will allow TSC to be used to compile JavaScript code. Simply add TSC to the build process and either run each JavaScript file through it, or keep running legacy JavaScript files through the build process and new TypeScript files through TSC.



With allowJs set to true, TypeScript will not check types in the current JavaScript code, but will transpile that code to ES3, ES5, or the version that is set as target in the tsconfig.json file using the module system you requested (in the module field json file). The first step has been completed. Commit it and pat yourself on the back - your codebase is now using TypeScript.



Step 2a: enable JavaScript type checking (optional)



Now that TSC is processing JavaScript, why not check its types? Even if there are no explicit type annotations, remember that TypeScript can infer types for JavaScript code in the same way as for TypeScript code. Include the required option in tsconfig.json:



{
     "compilerOptions": {
     "allowJs": true,
     "checkJs": true
}


Now, whenever TypeScript compiles a JavaScript file, it will try to infer and validate types the same way it does for TypeScript code.



If your codebase is large and when you enable checkJs it encounters too many errors at once, turn it off. Instead, enable JavaScript file checking one at a time by adding the // @ ts-check directive (a normal comment at the top of the file). Or, if large files throw a bunch of errors that you don't want to fix yet, leave checkJs enabled and add the // @ ts-nocheck directive for those files.



TypeScript cannot infer types for everything (for example, it does not infer types for function parameters), so it will infer many types in JavaScript as any. If you have strict mode enabled in tsconfig.json (recommended), then you may prefer to allow implicit any during migration. Add the following to tsconfig.json:



{
     "compilerOptions": {
     "allowJs": true,
     "checkJs": true,
     "noImplicitAny": false
}


Remember to turn on noImplicitAny again when you've finished migrating the bulk of your code to TypeScript. Doing so is likely to find many missed bugs (unless you are Zenidar, a disciple of the JavaScript witch Bavmorda, who can type-check with the power of his mind's eye with a wormwood potion).


When TypeScript executes JavaScript code, it uses a softer inference algorithm than TypeScript code. Namely:



  • All function parameters are optional.
  • The types of properties of functions and classes are inferred based on their usage (instead of having to be declared in advance):



    class A {
        x = 0 // number | string | string[],    .
         method() {
                 this.x = 'foo'
         }
         otherMethod() {
                 this.x = ['array', 'of', 'strings']
         }
    
    }
  • After declaring an object, class, or function, you can assign additional properties to them. Behind the scenes, TypeScript does this by generating an appropriate namespace for each function declaration and automatically adding an index signature to each object literal.


Step 2b: add JSDoc annotations (optional)



You may be in a rush and just need to add one type annotation for a new function that was added to an old JavaScript file. You can use the JSDoc annotation to do this until you can convert this file to TypeScript.



You've probably seen JSDoc before. These are comments like this at the top of the code with annotations starting with @, likeparam, @returns, and so on. TypeScript understands JSDoc and uses it as input to the type checker along with explicit type annotations.



Suppose you have a 3000 line service file (yes, I know, your "friend" wrote it). You add a new service function to it:



export function toPascalCase(word) {
       return word.replace(
             /\w+/g,
             ([a, ...b]) => a.toUpperCase() + b.join('').toLowerCase()
       )
}


Without a full conversion of utils.js to TypeScript, which will surely reveal a bunch of bugs, you can only annotate the toPascaleCase function, creating a small island of safety in a sea of ​​untyped JavaScript:



/**
     * @param word {string}    .
     * @returns {string}   PascalCase
     */
export function toPascalCase(word) {
     return word.replace(
           /\w+/g,
           ([a, ...b]) => a.toUpperCase() + b.join('').toLowerCase()
     )
}


Without this annotation, JSDoc TypeScript would infer the toPascaleCase type as (word: any) => string. Now, when compiling, it will know that the toPascaleCase type is (word: string) => string. And you get useful documentation.



For more detailed information on JSDoc annotations, visit the TypeScript Wiki (https://github.com/Microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript).



Step 3: rename files to .ts



Once you've added TSC to your build process and started optionally type checking and annotating JavaScript code wherever possible, it's time to switch to TypeScript.



File by file update file permissions from .js (or .coffee, es6, etc.) to .ts. Immediately after renaming files in the editor, you will see red wavy friends appear indicating type errors, missed cases, forgotten null checks, and typos in variable names. There are two ways to remove them.



  1. . , , , . checkJs, noImplicitAny tsconfig.json, any , , JavaScript .
  2. .ts tsconfig.json ( strict false), . any, . . , strict (noImplicitAny, noImplicitThis, strictNullChecks . .), . ( .)


, TODO any any, . , :



// globals.ts
type TODO_FROM_JS_TO_TS_MIGRATION = any

// MyMigratedUtil.ts
export function mergeWidgets(
       widget1: TODO_FROM_JS_TO_TS_MIGRATION,
       widget2: TODO_FROM_JS_TO_TS_MIGRATION
): number {
       // ...
}


Both approaches are quite relevant, and it's up to you to decide which one is preferable. TypeScript is a step-by-step typed language that is designed from the ground up to interact with untyped JavaScript in the most secure way possible. It doesn't matter if you interact with untyped JavaScript or weakly typed TypeScript, strongly typed TypeScript will always make sure the interaction is as safe as possible.



Step 4: activate rigor



Once a critical mass of JavaScript code has been ported, you'll want to make it all-in-one safe by using the stricter TSC flags one by one (see Appendix E for a complete list of flags).



When finished, you can turn off the TSC flags responsible for interacting with JavaScript, confirming that all your code is written in strongly typed TypeScript:



{
     "compilerOptions": {
     "allowJs": false,
     "checkJs": false
}


This will reveal any remaining type errors. Fix them and get a flawless, secure code base that most of OCaml's harsh engineers would pat you on the back for.

Following these steps will help you get far when adding types to the JavaScript code you control. But what about codes that are not controlled by you? Like the ones installed with NPM. But before we study this question, let's digress a little ...



Finding types for JavaScript



When you import a JavaScript file from a TypeScript file, TypeScript searches for type declarations for it using the following algorithm (remember, in TypeScript, “file” and “module” are used interchangeably):



  1. .d.ts , .js. .js.

    , :



    my-app/

    ├──src/

    │ ├──index.ts

    │ └──legacy/

    │ ├──old-file.js

    │ └──old-file.d.ts



    old-file ( ) index.ts:



    // index.ts

    import './legacy/old-file'



    TypeScript src/legacy/old-file.d.ts ./legacy/old-file.
  2. , allowJs checkJs true, .js ( JSDoc) any.




TSC-: TYPEROOTS ( )



TypeScript node modules/@types , (../node modules/@types . .). .



, typeRoots tsconfig.json , . , TypeScript typings node modules/@types:



{
     "compilerOptions": {
            "typeRoots" : ["./typings", "./node modules/@types"]
     }
}


types tsconfig.json, , TypeScript . , , React:



{
     "compilerOptions": {
             "types" : ["react"]
     }
}




When importing a third-party JavaScript module (the NPM package you installed in node modules), TypeScript uses a slightly different algorithm:



  1. Looks for a local type declaration for the module and, if one exists, uses it.



    For example, your directory structure looks like this:



    my-app /

    ├──node_modules /

    │ └──foo /

    ├──src /

    │ ├──index.ts

    │ └──types.d.ts



    This is how type.d looks like .ts:



    // types.d.ts
    declare module 'foo' {
          let bar: {}
          export default bar
    }


    If you then import foo, then TypeScript uses the external module declaration in types.d.ts as the type source for it:



    // index.ts

    import bar from 'foo'
  2. package.json, . types typings, .d.ts, , .
  3. Or it will cycle through the directories looking for the node modules / @ types directory, which contains the type declarations for the module.



    For example, you installed React:



    npm install react --save

    npm install @ types / react --save-dev



    my-app /

    ├──node_modules /

    │ ├── @ types /

    │ │ └──react /

    │ └── react /

    ├──src /

    │ └──index.ts



    When importing React, TypeScript will find the @ types / react directory and use it as the source of type declarations for it:



    // index.ts

    import * as React from 'react'
  4. Otherwise, it will go to steps 1–3 of the local type search algorithm.


I have listed quite a few steps, but you will get used to them.



about the author



Boris Cherny is Chief Engineer and Product Leader at Facebook. Previously worked at VC, AdTech and a variety of startups, most of which do not exist today. He is interested in programming languages, code synthesis and static analysis, and strives to share his experience with digital products with users. In his free time, he organizes TypeScript club meetings in San Francisco and maintains a personal blog - performancejs.com . You can find Boris's GitHub account at github.com/bcherny .



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

» Table of Contents

» Excerpt



For Habitants 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