Rating: 8.0/10.
Programming TypeScript: Making Your JavaScript Applications Scale by Boris Cherny
Book about the TypeScript language, which adds a strong typing system and a type inference engine to JavaScript. The material is fairly advanced and assumes a solid understanding of JavaScript features. It covers many advanced TypeScript features, including a rough sketch of how they are implemented.
Chapter 1,2. JavaScript by itself, does not have a strong type system and permits many implicit type conversions that are often unintended; conversely, TypeScript has a much stronger type system that can catch numerous errors. The TypeScript compiler (TSC) translates the code into JavaScript while checking types, ensuring that any type annotations do not impact the program’s execution. TypeScript infers types statically, unlike JavaScript’s dynamic approach, and if there is any mismatch, it will trigger an error at compile time.
TypeScript can infer the types of most elements without the need for explicit declaration. For instance, using the let
keyword will infer a static type. Various options for setup and configuration are available in tsconfig.json
and tslint.json
.
Chapter 3. Some of the most common types: the any type matches anything, so it will never raise an error; it is inferred when there is insufficient information to assign a more specific type. The unknown type, which is explicitly annotated to be any type when the exact type is not known, allows only very basic operations, such as comparison for equality, applying more specific operations, like addition, to an unknown type will result in an error.
Some basic types include booleans, numbers (which encompass both integers and floats), and strings. A type literal represents a single value and nothing else; for example, an integer that must be 42 and cannot be any other integer.
In TypeScript, objects are structurally typed, meaning that the type is determined by its internal structure and contents rather than the name of its class; thus, the type of an object can be a deeply nested structure itself. The object type can be inferred automatically from a constant literal, or you can use definite assignment to declare the type first and assign the value later. A variable that is declared but not assigned a value has a type of undefined and will generate a type error if you attempt to access it.
Objects can have optional properties or index signatures, which allow arbitrary property names that fit a given pattern. Although a const object can still have fields that are mutable, you can use the readonly keyword to specify a field that cannot be modified. An empty object has a type that is often confusing, so its use should be avoided.
The type keyword in TypeScript is used to define type aliases, like a complex object structure, and give a name to it. A union type represents a variable that can be one of two types. Arrays generally have homogeneous types of elements; TypeScript can automatically infer the type of each element as you push elements into it. Tuples are subtypes of arrays; they allow heterogeneous types, specify the length of the tuple, and can also include optional elements.
TypeScript has several valid types that represent empty values. undefined
is assigned to a variable that is declared but not yet defined, which is different from null
, signifying the absence of a value. void
is the type of a function that doesn’t return anything, while never
is the type of a function that doesn’t return at all.
Enums in TypeScript can map a string to either a string or a number, but they have a number of surprising properties, such as implicit conversion into an integer when the enum is constant. Therefore, it’s recommended to avoid using them.
Chapter 4: Functions. You can annotate the parameters and return values of a function, including those that are optional. It often infers values from return statements and default values. Some functions take rest parameters, which are passed in as an array of any type. The JavaScript “this” variable behaves in confusing ways, so you can specify the type of the expected “this” parameter to error if it’s used incorrectly.
The type of a function can also be expressed using arrow syntax. This is useful for annotating types of variables that represent functions, for example, in higher-order functions. This syntax is considered type-level code when it deals only with types, not values.
Polymorphic type parameters, or generics, are useful for expressing types of functions that are generic, like “map” or “filter”. The concrete type of a generic type is inferred when the function is used. Bounded polymorphism is used to express when a type can be any type that extends a defined type.
Chapter 5: Classes and Interfaces. You can specify access modifiers, such as public, private, and protected, on class properties; in cases of inheritance, this approach allows subclasses to return objects of the right type. An interface is similar to a type alias but is better for representing object types; multiple interfaces can be defined with the same name, and they are automatically merged. When a class implements an interface, the compiler checks that all the required methods are correctly typed.
Classes are structurally typed, meaning identically shaped objects will have the same type, which is different from the nominal typing used in many languages. Additionally, classes can have generic types. Some more advanced meta-programming constructs include typing for mixins and decorators, but these are more experimental.
Chapter 6: Advanced Types. The definition of a subtype is: if B is a subtype of A, then you can use B anywhere A is required. This is straightforward for primitive types but becomes quite tricky in cases involving objects, generics, and functions. Most cases of assignment are covariant, meaning you can substitute a subtype for an object but not a supertype. However, an exception exists for function parameters; these are contravariant, so for a function to be a subtype of another function, its parameters must be a supertype of the original function’s parameters.
The types of mutable variables are automatically widened to have a more basic type, whereas constants remain the literal type. The compiler also uses the excess property check heuristic to help catch bugs when you pass extra object parameters as a literal to a function that aren’t expected; technically, this is a subtype and is usually permissible. The compiler keeps track of type refinements as control flow proceeds through a series of if statements; for example, if it checks whether a value is null, then inside the statement, that type will no longer include null.
Totality checks ensure that all code paths are handled and return a value. The keyof operator of an object returns a union of all the string literal types.
There are two ways to create map/dict types. The first, a record type, is a map from key to value, used when keys are not known in advance; however, if the keys are known statically, a mapped type is more suitable, as it can statically check that all expected keys are present. Mapped types can also be used to derive types from an object, like Readonly<object>
.
Conditional types are types that depend on the type of something else and are inferred statically; the infer keyword is used to assign this conditional type of a generic to a type variable.
There are some convenient escape hatches for when you don’t have time to prove to TypeScript that your code is correct. For instance, a type assertion, like x as string
, forces the type to be string. Similarly, the !
operator forces a variable to be non-null or defined, which is useful when you’re certain of a variable’s state, but the compiler cannot infer it.
Chapter 7. Various methods for handling errors. While returning null is the simplest approach, it lacks the ability to convey why a failure occurred. Throwing an error is always a possibility without affecting types; unlike Java, where the caller is required to properly handle an error, unless explicitly declared in the function’s return type, the caller isn’t obliged to handle it properly.
The Option type is a container that may or may not contain a value. This type includes a flatMap method, allowing for the chaining of operations without the need to handle null at every step.
Chapter 8. The JavaScript event loop, used in functions like setTimeout or with promises places tasks in a queue and executes them one by one, enabling concurrency but not parallelism. Handling callbacks can be complex, as managing the order of events is challenging and often leads to a “pyramid of doom” — a situation where a sequence of callbacks results in deeply nested code structures. Therefore, ‘async/await‘ is introduced as syntactic sugar over promises.
For achieving parallelism, recommend using web workers, these use a message-passing system for communication between workers. For typing in this pattern, it’s recommended to use a mapped type Event that represents all possible events a worker may receive.
Chapter 9. TypeScript can handle DOM functions and its integration with frameworks like React and Angular, eg: TypeScript ensures that required properties of JSX tags are present, and it recognizes that the render function returns JSX elements. For integrating frontend and backend code, it’s useful to use code-generated APIs, such as Swagger, to keep frontend and backend types synchronized, even when they’re written in different languages.
Chapter 10: Historically, JavaScript has utilized several different module systems, such as CommonJS, but ES2015 is now the recommended system. Generally, importing and exporting using ES2015 ensures types are handled correctly. For dynamic imports (await import), which are necessary for lazy loading and take a string as the module name, these need to be marked to ensure static analyzability. Namespaces are another mechanism that automatically merges declarations in the global scope. However, it is recommended to use modules instead when possible.
Chapter 11: When interoperating with JavaScript, a type declaration file, denoted as .d.ts
, contains only the types of exported variables. This is useful for untyped JavaScript code, where you need to annotate only the exports for TypeScript use, rather than annotating everything. Ambient declarations allow you to declare variables or types in the global scope without the need for an explicit import.
To migrate a project from JavaScript to TypeScript, start by adding the TypeScript compiler (tsc) in its least strict mode that doesn’t perform type checking. Then, gradually set flags to enable stricter checking. Initially, untyped JavaScript code can utilize a lenient inference algorithm that allows extensive use of the Any type. Once most of the code is annotated, you can configure the flags for strict mode.
Some Node modules come with separate type declaration libraries, or community-created ones in the DefinitelyTyped repository.
Chapter 12: The TypeScript compiler (tsc) supports multiple compiler targets to accommodate older versions of JavaScript. However, it does not automatically polyfill features that are not available. To address this, you need to install relevant polyfill packages and configure TypeScript accordingly. Source maps are beneficial for debugging, as they link the JavaScript code back to the original TypeScript code. For running TypeScript in the browser, a bundler like Webpack is required.