typescript-for-beginners-the-ultimate-guide-9781032067582-9781000539967-9781032067575-9781003203728-1032067586
typescript-for-beginners-the-ultimate-guide-9781032067582-9781000539967-9781032067575-9781003203728-1032067586
Reasonable efforts have been made to publish reliable data and information, but the author and publisher
cannot assume responsibility for the validity of all materials or the consequences of their use. The authors
and publishers have attempted to trace the copyright holders of all material reproduced in this publication
and apologize to copyright holders if permission to publish in this form has not been obtained. If any
copyright material has not been acknowledged please write and let us know so we may rectify in any future
reprint.
Except as permitted under U.S. Copyright Law, no part of this book may be reprinted, reproduced,
transmitted, or utilized in any form by any electronic, mechanical, or other means, now known or hereafter
invented, including photocopying, microfilming, and recording, or in any information storage or retrieval
system, without written permission from the publishers.
For permission to photocopy or use material electronically from this work, access www.copyright.com or
contact the Copyright Clearance Center, Inc. (CCC), 222 Rosewood Drive, Danvers, MA 01923, 978-750-
8400. For works that are not available on CCC please contact [email protected]
Trademark Notice: Product or corporate names may be trademarks or registered trademarks and are used
only for identification and explanation without intent to infringe.
DOI: 10.1201/9781003203728
Acknowledgments
About the Author
APPENDIX
INDEX
Acknowledgments
There are many people who deserve to be on this page, for this book would not
have come into existence without their support. That said, some names deserve a
special mention, and I am genuinely grateful to:
Sufyan bin Uzayr is a writer, coder, and entrepreneur with more than a decade of
experience in the industry. He has authored several books in the past, pertaining
to a diverse range of topics, ranging from history to computers/IT.
Sufyan is the Director of Parakozm, a multinational IT company specializing
in EdTech solutions. He also runs Zeba Academy, an online learning and
teaching vertical with a focus on STEM fields. He specializes in a wide variety
of technologies, such as JavaScript, Dart, WordPress, Drupal, Linux, and
Python, and he holds multiple degrees, including ones in management, IT,
literature, and political science.
Sufyan is a digital nomad, dividing his time between four countries. He has
lived and taught in universities and educational institutions around the globe. He
takes a keen interest in technology, politics, literature, history, and sports, and in
his spare time, he enjoys teaching coding and English to young students.
Learn more at sufyanism.com.
CHAPTER 1
TypeScript
Introduction to TypeScript
DOI: 10.1201/9781003203728-1
TypeScript (TS) is a typed extended set of JavaScript (JS) that compiles to plain
JS. As an analogy, if JS was CSS, then TS would be SCSS.
All valid JS code you write is also valid TS code. However, by using TS, you
get to use static typing and the latest features that compile into simple JS that is
supported by all browsers. TS aims to solve the JS scaling problem, and it works
pretty well.
In this book, you'll start by reading about the various features of TS and why
learning it is a good idea. The rest of this book will focus on installing and
compiling TS, along with some popular text editors that offer you support for TS
syntax and other important features.
WHAT IS TS?
TS is a kind of updated version of the JS language. It can run on Node.js or any
web browser that supports ECMAScript 3 or higher. TS is a statically compiled
language that provides optional static typing, classes, and an interface. It allows
you to write simple and clean JS code. So, adopting TS can help you create more
easily deployable and more reliable software.
A BRIEF HISTORY OF TS
However, it would seem that there is no need for another programming language
for the client side in the web environment, if traditional JS, which is used on
almost every site, which is owned by many developers and whose support in the
programming community is quite high, also copes with all the same work. But
TS is not just a new JS.
First, it should be noted that TS is a strongly typed and compiled language,
which may be closer to programmers of Java, C#, and other strongly typed
languages, although the output of the compiler creates the same JS, which is then
executed by the browser. However, strong typing reduces the number of
potential errors that could occur when developing in JS.
Second, TS implements many of the concepts that are common to object-
oriented languages, such as inheritance, polymorphism, encapsulation, access
modifiers, and so on.
Third, the potential of TS makes it faster and easier to write large complex
programs, and therefore easier to maintain, develop, scale, and test them than in
standard JS.
Fourth, TS develops as an opensource project and, like many projects, is
hosted on GitHub. Repository address –
https://github.com/Microsoft/TypeScript. In addition, it is cross-platform, which
means that we can use both Windows and macOS or Linux for development.
At the same time, TS is a superset of JS, which means that any JS program is a
TS program. In TS, you can use all the constructs that are used in JS – the same
operators, conditional, cyclic constructs. Moreover, the TS code is compiled in
JS. Ultimately, TS is just a tool that is designed to make application development
easier.
Although this language does not provide additional functionality in the
runtime, it offers a number of features that help developers write more reliable
and easier-to-maintain code than in the case of pure JS.
let jsVar = 0;
jsVar = "js";
In the case of TS, you can restrict this behavior by declaring the type for the
variable explicitly. As a result, if you try, for example, to assign a string to a
variable of type number, an error will occur.
In fact, this is what distinguishes TS from JS. It uses types, which allows us to
avoid stupid errors in the code.
In the JS example above, nothing prevents the developer from presenting the
object via the aNumber variable. This approach, although it will not cause the
program to crash, will completely eliminate the possibility of self-documenting
the code at the expense of variable names.
TS easily solves this problem by defining the type of a variable when it is
declared, without further assigning it to a value of another type.
If this variable is later accessed by another developer, they can be sure that its
value is a number, as the name suggests.
function isEligible(personObj) {
return personObj.age > 34;
}
let john = {
name: "Josh",
age: 23
};
isEligible(john);
In this case, the isEligible function expects an object with the age field. But in
JS, there is no way to guarantee that the passed argument will be exactly an
object, or that it will contain the age field.
Again, TS has a solution for this.
interface Person {
name: string;
age: number;
}
function isEligible(personObj: Person) {
return personObj.age;
}
let john = {
name: "Josh",
age: 23
};
isEligible(john);
So far, this code may not be clear to you. But note that it guarantees that the
passed variable has the type Person, which is defined at the beginning.
Using TS will save you from hundreds of careless errors in the code, which
sometimes turn out to be so stupid that you want to tear your hair out. In
addition, your code will become better self-documented and easier to maintain.
If you didn't have enough auto-substitution options for JS code in the
integrated development environment (IDE), then you should get acquainted with
TS all the more. The presence of types gives this language the ability to offer
more precise substitutions in the IDE.
TS AS COMPARED TO JS
What Is JS?
JS was introduced in 1995 as a loosely typed scripting language developed by
Netscape to add more dynamics to HTML pages. But the web has changed a lot
since then. What were simple pages with a single form in 1995 are now large
and complex web applications. JS was not originally designed for developing
complex enterprise applications. Currently, most browsers support the
ECMAScript 5 standard. That is, usually when someone says “JavaScript,” he
means the language of the ES5 standard, although ES5 is not the latest version of
the standard. In 2015, the 6 standard ES6 was introduced, in 2016 — ES7 or ES
2016. But not all browsers fully support the new features.
The incompatibility problem can be solved by using transpilers such as Babel
or using the TS language.
TS Strengths
The main argument in favor of TS is strict static typing. So, what is the
difference between static and dynamic typing? When using static typing, less
documentation is required, in fact your code has better documentation. For most
of the code, it is very useful that the types of arguments and results of the
function execution are documented, always knowing exactly what to expect at
the input and output.
Moreover, when you look at your old JS code, many nuances are not clear,
you need to study the call chain in detail to understand what type of result will
be returned in a certain function. Of course, you can use JSDoc, which allows
you to write inline documentation, but it needs to be constantly updated. When
using TS, you only have one source of truth.
Also, the advantage of static typing is the possibility of autocompletion in
code editors. Writing code becomes easier, because you don't have to constantly
go through the documentation and look for the name of the method you need.
Also, refactoring becomes less painful, as the compiler will tell you if
something is broken, which will avoid runtime errors. But still there are a couple
of drawbacks you need more time to learn and write code at the initial stages, an
additional layer of complexity appears, freedom of thought is limited, you cannot
just add a property or assign a value of another type. Strict typing does not
guarantee that there are no errors in the program.
If you are not familiar with TS/ES6/ES7 and you need to write a small web
application from a couple of pages, the fastest way to do this is to use regular JS
(ES5). But it is also worth noting that ES5 is considered an outdated
(deprecated) version of JS, so it is better for your project and career in general to
go deeper into learning the new ES6, ES7, and TS standards. At first, you will be
intimidated by all these assemblers and transpilers, but your training costs will
be justified if you want to stay in the trend of web development. If you work in a
team and you need to create a scalable product that will need to be supported for
a long time, write it in TS. Since ES6/ES7 now needs to be translated to ES5,
you can use TS instead of Babel, so you will maintain type security and your
code will work in all browsers.
TS brings many benefits to performance and to the developer experience. TS
is not unique to Angular; other powerful integrated environments, such as React
and Vue, are starting to use TS to allow development teams to build applications
that are robust, resilient, and scalable. JS and TS are constantly evolving but do
not compete with each other. TS was created to complement and improve JS, not
replace it. In the future, they may become very similar in function, but for now,
TS remains a statically typed alternative.
Syntactic Sugar
TS provides a very simple syntax for checking the type of an object at compile
time. This syntax is known as syntactic sugar, or, more formally, type
annotation. Consider the following version of our JS source code, written in TS:
Notice that in the first line of this snippet, we entered a colon: and the string
keyword between our variable and its assignment. This type annotation syntax
means that we set the type of our variable as a string type and that any code that
does not adhere to these rules will result in a compilation error. Running the
previous code through the TS compiler will cause two errors:
The first mistake is pretty obvious. We have specified that the test variable is a
string, and therefore trying to assign a number to it will cause a compilation
error. The second error is similar to the first one and, in fact, says that we can't
assign the function to a string.
Thus, the TS compiler introduces strong or static typing in our JS code, giving
us all the advantages of a strongly typed language. Therefore, TS is described as
an extended JS variant.
TS STRUCTURE
Basic TS Types
TS is a statically typed language. The type cannot be changed during program
execution. This allows you to reduce a large number of errors and identify many
of them even at the compilation stage.
TS has several simple data types: numbers, strings, structures, Boolean. It
supports all types that are available in JS, complementing the convenient enum
type.
Boolean
The most basic type is the Boolean true/false, which is called Boolean in JS and
TS.
console.log(isEnabled);
console.log(isAlive);
Number
Unlike various object-oriented languages such as Java, C#, and C++, which
provide different data types, such as int, float, and decimal, for storing a numeric
value, TS has only one numeric data type, called number. A variable with the
number data type can contain any numeric literal with floating, hexadecimal, and
binary or octal values.
String
Another important part of programs in web pages and servers is text data. As in
other languages, TS uses the same “string” notation for such data. Like JS, TS
uses double (“) or single (‘) quotes to frame text data.
let name: string = "bob";
name = 'smith';
You can also use strings with templates, which can be multi-line and have built-
in expressions. These strings are surrounded by back apostrophes or quotation
marks (’) and inline expressions are denoted as ${expr}.
Array
TS, like JS, has arrays of values. The array type can be defined in one of two
ways. The first is to denote the type of array elements before []:
Tuple
The Tuple type gives you the ability to declare an array with a known fixed
number of elements that do not have to be of the same type. For example, you
want to have the Tuple value as a pair of “string” and “number”:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
When an element with an idex outside the known range is received, the Union
type is returned:
The Union type will be described later in the Advanced types section.
Tuples Deconstruction
Since tuples use array syntax, they can be deconstructed or disassembled in two
ways. The first one, using a simple array syntax, looks like this:
console.log('tupleType[0]: ${tupleType[0]}');
console.log('tupleType[1]: ${tupleType[1]}')
Here we simply write each property of the tupleType variable to the console,
referring to the index in the array, i.e., tupleType[0] and typleType[1]. The
output of this code will be as follows:
So, we created a tuple with a string and a Boolean value and deconstructed it
using array syntax. Note that since we use the array syntax, we can request the
third property of this tuple:
console.log('tupleType[2]: ${tupleType[2]}');
Since our tuple does not have a third property, typleType[2] will be undefined,
as can be seen from the output of this line of code:
tupleType[2]: undefined
This is clearly far from perfection. The best way to deconstruct a tuple is to use
the array syntax to create the corresponding tuple on the left side of the
assignment:
Here we define an array of two elements named t1 and t2 and assign this array a
tuple value. Then we write t1 and t2 to the console. The output of this code looks
like this:
t1: test
t2: false
Here we are trying to deconstruct our tuple of two properties into a tuple of three
properties. The compiler will throw an error in this case:
error TS2493: Tuple type '[string, boolean]' with length '2' cannot be assigned
to tuple with length '3'
optionalTuple: test2,true
optionalTuple: test
Enum
Enum is a special type borrowed from other languages like C#, C ++, and Java
that provides a solution to the special numbers problem. Enum binds a human-
readable name for a specific number. As in languages like C#, the enum type is a
more convenient way to give clear names to a set of numeric values.
By default, enums start with 0. You can change this by directly specifying a
value for one of the enum members. For example, we can start the previous
example with 1 instead of 0:
A convenient feature of enumerations is that you can also get the name of an
enumeration member by passing its numeric value. For example, if we have a
value of 2 and we want to see what it corresponds to in the Color enumeration
described above, we can do it like this:
enum DoorState {
Open,
Closed,
Ajar
}
Here we have defined an enum named DoorState to represent the state of the
door. Valid values for this door state are Open, Closed, or Ajar. Under the hood
(in generated JS), TS will assign a numeric value to each of these human-
readable enumeration values. In this example, the value of the DoorState
enumeration.Open will be equal to the numeric value 0. The same is true for the
DoorState enumeration.Closed will be equal to the numeric value 1, and the
value of the enum is DoorState.Ajar will be equal to 2. Let's take a quick look at
how we'll use these enum values:
Here, the first line of this code snippet creates a variable named openDoor and
sets its value to DoorState.Open. The second line simply writes the value of the
openDoor variable to the console. The output will look like this:
openDoor is: 0
This clearly shows that the TS compiler has replaced the value of the DoorState
enumeration.Open to the numeric value 0.
Now let's use this enumeration in a little different way:
This code snippet uses the string value “Closed” to search for the enum type and
assigns the resulting enum value to the closedDoor variable. The output of this
code will be as follows:
closedDoor is: 1
This example clearly shows that the enum value for DoorState is.Closed is
combined with the enum value for DoorState [“Closed”], since in both cases the
numeric value 1 is returned. Enumerations are a convenient way to define an
easy-to-remember, easy-to-read name for a special number. Using human-
readable enumerations instead of just throwing around various special numbers
in our code makes the purpose of the code more clear. Use a value for the entire
application named DoorState.Open or DoorState.Closed is much easier than
remembering to set the value to 0 for Open, 1 for Closed, and 3 for Ajar. In
addition to making our code more readable and more understandable, using
numbers also protects our code base every time these special numeric values
change, since they are all defined in one place. The last thing I would like to
mention about enumerations is that we can set the numeric value manually, if
necessary, as follows:
enum DoorState {
Open = 3,
Closed = 7,
Ajar = 10
}
Here we have redefined the default enum values to set the value DoorState.Open
equal to 3, DoorState.Closed equal to 7 and DoorState.Ajar equal to 10.
String Enums
Another variant of the enum type is a string enumeration, in which numeric
values are replaced with strings:
enum DoorStateString {
Open = "open",
Closed = "closed",
Ajar = "ajar"
}
var openDoorString = DoorStateString.Open;
console.log('openDoorString = ${openDoorString}');
Here we have an enum named DoorStateString, where each of the enum values
is now of type string. The output of this code snippet will be as follows:
openDoorString = open
Any
We may need to describe the type of variables that we don't know when we write
our application. These values can be obtained from dynamic content, such as
from a user or from a third-party library. In these cases, we want to disable type
checking and allow the values to pass validation at compile time. To do this, you
need to use the any type:
The any type is a powerful way to work with existing JS, which allows you to
gradually include more and more type checks at compile time. You can expect
Object to play the same role as it does in other languages. But variables of the
Object type only allow you to assign them any value. You can't call undeclared
methods from them, even those that may exist at the execution stage of the
program:
The any type can also be useful if you know some part of the variable type, but
not all of it. For example, you can have an array with elements of different types:
let list: any[] = [1, true, "free"];
list[1] = 100;
Void
Void is the opposite of Any: the absence of any types. It is most often used as
the return type of functions that do not return any value.
Declaring variables with the void type is useless, because you can only assign
them undefined or null values:
If you declare a variable of type null or undefined, then such a variable can only
be assigned the value null or undefined, respectively, which has no practical
application.
It is worth noting that if you use the –strictNullChecks compiler directive, null
and undefined can only be assigned to a variable of type void and to variables of
type null or undefined, respectively. This helps to avoid a lot of mistakes. In this
case, if the variable needs to be assigned a value with the string or null or
undefined type, you can use the string | null | undefined union type.
Never
The never type represents a type whose value never occurs. For example, never
is a type that returns a function that always throws exceptions or that never exits
(for example, an infinite loop). Variables can also have this type, for example, in
order to never take the value true.
The never type is a subtype of any type. A variable of type never can be
assigned to a variable of any other type. On the other hand, there is no such type
that will be a subtype of this type, just as a variable of this type cannot be
assigned anything other than a variable of the same type (never).
Symbol
The Symbol type is primitive and corresponds to the same type in JS. This type
provides unique identifiers that can be used as keys for object properties.
Values of the Symbol type implement a global ‘Symbol’ object, which has a
set of methods and properties that can be called as functions.
Type Assertions
Sometimes you find yourself in a situation where you know more about the
value of a variable than TS does. This usually happens when you know that the
type of an entity may be more specific than its current type.
Type assertion is like typecasting in other languages, but it doesn't do any
special checks or data restructurings. The type conversion has no effect at the
execution stage of the program and is used only by the compiler. TS assumes
that the programmer will do all the necessary checks that are required.
The type conversion can be done in two ways. The first is the use of angle
brackets syntax:
The two examples given are completely similar. To use one or the other is by-
and-large a choice of preference; however, when TS is used in conjunction with
JSX, only typecasting via syntax is allowed.
TS Compiler
The TypeScript compiler (also known as “tsc”) is the basis of many packages,
plugins, and tools that work with TS, although we don't often call the tsc
directly, since we configure the behavior of the tsc inside the packers we use.
When we install TS globally npm i-g TS, we get a tool called tsc. If we run
this command with the help tsc –help flag, we will see a large number of options
that go with it. The main ones that you should pay attention to are the first
examples at the top of examples:
tsc hello.ts
tsc --outFile file.js file.ts
tsc @args.txt
tsc --build tsconfig.json
If we create a hello.ts file, write const four:number = 2 + 2 inside, and then run
tsc hello.ts, we get a compiled file hello.js with the value var four = 2 + 2;.
We will rarely use tsc in this way, because tsc is included in plugins such as
Webpack, Parcel, and Angular CLI.
Instead, we use tsc –init. It creates a file in the root directory named tsconfig.
json. It contains all the default values. This is very convenient, because instead
of defining all these parameters on the command line when it is called, we define
them in this TS configuration file. When we run the tsc command, the compiler
will take the settings from tsconfig. json.
Also, we can change the configuration settings, for example, change the value
of ourDir to ./dist. Now all compiled files should be saved to an external
directory ./dist.
Everything defined in this file will specify which parameters the TS compiler
will use when compiling. When TS starts “complaining” about not using enough
types, you can start disabling some of these warnings or errors.
Variable Declarations
Let and const are relatively new types of variable declarations in JS. As we
mentioned earlier, let is similar to var in some ways, but allows users to avoid
some of the common errors encountered in JS. const is an extension of let that
prevents variables from being overridden.
Since TS is an add-on to JS, the language also supports let and const. Next,
we'll go into more detail about these new variable declarations and explain why
they are preferred over var.
var Declarations
The variable declaration in JS always occurs using the var keyword.
var a = 10;
As you probably understood, we just declared a variable with the name a and the
value 10.
We can also declare a variable inside a function:
function f() {
var message = "Hello, world!";
return message;
}
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // returns 11;
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f (); / / returns 2
Scoping
The var declaration has some strange scope rules for those using other
programming languages. See the following example:
return x;
}
Some may need to re-look at that example. The variable x was declared inside
the if block, and we can access it outside of that block. This is because var
declarations are available anywhere inside the function, module, namespace, or
global scope that contains them, regardless of the block in which they are
contained. Some call it var-visibility or function-visibility. The parameters are
also visible inside the function.
These scope rules can cause several types of errors. One annoying problem is
that it is not an error to declare a variable multiple times:
return sum;
It's probably easy to see that the internal for loop will accidentally overwrite the
variable i, because i has scopes inside the sumMatrix function. Experienced
developers know that similar errors slip through code review and can be the
cause of endless frustration.
Block Viewport
When a variable is declared using let, it is used in block scope mode. Unlike
variables declared with var, whose scopes extend to the entire function they are
in, block-scoped variables are not visible outside their nearest block or for loop.
if (input) {
// Here we see the variable 'a'
let b = a + 1;
return b;
}
Here we have two local variables a and b. The scope of a is bounded by the body
of the function f, while the scope of b is bounded by the if condition block.
Variables declared in the catch block have the same visibility rules.
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
Another property of block scope variables is that they cannot be accessed before
they are declared. While block scope variables are represented everywhere in
their block, there is a dead zone at each point before they are declared. It's just a
way of saying that you can't access them before the let statement, and luckily TS
will remind you of that.
However, you can still close a variable with a block scope before declaring it.
However, an attempt to call such a function before declaring it will result in an
error. If you compile to the ES2015 standard, this will cause an error; however,
right now TS allows this and will not indicate an error.
function foo() {
//okay to capture 'a'
return a;
}
let a;
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
In the example above, all the declarations of x actually point to the same x,
and this is perfectly acceptable. This is often a source of bugs. So, it's a good
thing that the let declarations don't allow this.
let x = 10;
let x = 20; / / Error: cannot redefine 'x' in the same scope
function f(x) {
let x = 100; / / error: intersects with function parameter
}
function g() {
let x = 100;
var x = 100; / / error: cannot declare 'x' twice
}
This does not mean that a variable with block scope cannot be declared with a
variable with scope in the same function. A variable with a block scope just
needs to be declared in its block.
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
The method of introducing a new name in a nested area is called hiding. This is a
kind of two-bladed sword, because it can introduce some bugs, as well as get rid
of others. For example, imagine how we could rewrite the sumMatrix function
using the let variables.
return sum;
}
This version of the loop does the summation correctly, because the i of the inner
loop overlaps the i of the outer one.
Such concealment should usually be avoided, so that the code is cleaner. But
in some scenarios, this method may be ideal for solving the problem. You should
use the best solution at your discretion.
function myFavoriteCity() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
Because we have captured the city variable from its environment, we can still
access it, despite the fact that the if block has finished executing. Remember our
previous example with setTimeout. We ended up needing to use IIFE to capture
the state of a variable for each iteration of the for loop. As a result, we created a
new variable environment each time for our captured ones. This was a bit of a
pain, but thankfully we won't need to do it again in TS.
Let declarations behave very differently when they are part of a loop. Instead
of introducing a new environment for the loop, they introduce a new scope for
each iteration. Since this is what we did with our IIFE, we can change our old
setTimeout example using let declarations.
Constant Declarations
Constant declarations are another way to declare variables.
const numLivesForCat = 9;
They are the same as let, only, according to their name, their value cannot be
changed after they have already been assigned a value once. In other words, all
the let scope rules apply to them, but you can't reassign them. The value they are
associated with is immutable.
Let or Const?
We have two ways of declaring with similar rules for their scope, so it begs the
question of which one to use. The answer will be the same as for most broad
questions: it depends on the circumstances.
Applying the principle of the lowest level of privileges, all variable
declarations that you do not plan to change in the future should use const. This is
because if a variable should not change its value, other developers who are
working on the same code should not be able to write the object. This should
only be allowed if there is a real need to reassign the variable. Using const
makes the code more predictable and understandable when explaining the data
flow.
Destructuring
Another innovation from the ECMAScript 2015 standard, which is in TS, is
destructuring.
The simplest form of destructuring is using an array:
This creates two new variables named first and second. In essence, this is the
equivalent of an index call, just more convenient:
first = input[0];
second = input[1];
// swap variables
[first, second] = [second, first];
You can create a variable for the remaining list items using the …name syntax:
Destructuring an Object
You can also destruct objects as shown in the following example:
let o = {
a: "foo",
b: 12,
c: "bar"
}
let {a, b} = o;
This code creates new variables a and b from o.a and o.b. Note that you can skip
c if you don't need it.
Renaming Properties
You can also give different names to the properties:
If this syntax is a little confusing to you, you can write it in a different way to
make it clearer:
The confusing thing is that the colon here does not denote the type. The type, if
you specify it, still needs to be written after destructuring:
Default Values
Default values allow you to define a property, even if it was not set:
Declaring Functions
Destructuring also works with function declarations; you can see an example
below:
Then, you should remember to give the default value for the optional properties
of the destructured parameter when defining the function. Remember also that c
was defined with the optional property b:
Use destructuring with caution. As the previous example showed, all complex
destructuring expressions have many nuances. This is especially true for multi-
level nested destructuring, which is really hard to understand even without
renaming, default values, and type annotations. Try to keep the destructuring
expressions small and simple.
Interfaces
You can see that TS has obtained a lot from C#. Interfaces will be another
example of such close collaboration.
An interface is a declaration that is similar to a class but does not have a
method implementation. You can use it to describe the properties and methods of
objects. At the same time, the interface does not have the implementation of
functions and does not have the code itself – it is only necessary for the compiler
to evaluate your implementations of the object (let me remind you that the class
is also an object). Roughly speaking, an interface is a descriptive structure.
Unlike classes, interfaces are uncompilable and live only in TS runtime.
The interface declaration starts with the interface keyword. Then, the name of
the interface comes, which is usually started with a capital letter I. This is not a
prerequisite for using interfaces, but I recommend that you stick to this
convention, if only because it is very convenient.
You can write interfaces anywhere: next to the code, at the beginning of a file,
or even in a separate file. I recommend keeping the interface declaration where it
is needed. For example, if the interface is used only within a single module (file),
then you can safely keep it there. If the interface is used in many modules of the
project, it is more appropriate to create a directory type and decompose the
interfaces according to the logic there.
Let's consider the simplest example of an interface for an object describing a
VPS server:
interface IServer {
hostname: string;
location: string;
active: boolean;
public_address: string;
}
Now that we have defined the interface, we can use it in a variable. It is usually
customary to say “implement the interface” rather than “use the interface,” since
the compiler verifies that the interface is implemented correctly. And, in case of
an incorrect implementation (there is not at least one property), it will output
errors every time you do something wrong. For example, let's forget to specify
the public_address property.
The compiler evaluated our initiatives, but did not see the public_address
property in our object, that is why it shows an error.
But some structure cannot always be distinguished using only one interface.
Of course, in TS, you can use one interface as a type for a property of another
interface.
interface IPublicAddress {
netmask: string;
gateway: string;
address: string;
}
interface IServer {
hostname: string;
location: string;
active: boolean;
public_address: IPublicAddress;
}
Note that in addition to primitive types and other interfaces, you can describe
functions in interfaces. This is done using arrow functions, for example, like this:
interface IServer {
getPublicAddres: () => IPublicAddress;
}
At the same time, no one forbids you to specify the parameters of the function:
interface ICalculator {
sum: (a: number, b: number) => number;
}
Extending Interfaces
In TS, you can only extend interfaces, not inherit them. The extension is used if
you need a new interface to have not only all the properties of an interface, but
also have additional or unique properties for that interface.
interface IResponse {
status: number;
}
interface ICache {
size: number;
first: ICacheItem;
last: ICacheitem;
items: {
[item: string]: ICacheItem;
};
}
Now you can write any value to the items object that has a string type key and an
ICacheItem type value.
Interface Implementation
In the case of an object, you simply assign a colon-separated variable to the
interface you need as a type and implement it. With classes, it is somewhat
different – classes must implement interfaces. If you forget to write an
implementation of at least one method, the compiler will throw an error. For
example, we will write a cache interface that will be implemented by the class.
interface ICacheItem {
mtime: number;
content: string;
}
interface IFileCache {
set: (key: string, value: ICacheItem) => void;
get: (key: string) => ICacheItem;
}
The above is the simplest example of implementing a class, from which it is not
entirely clear why an interface is needed here at all, when we could just create a
class. Let's get this straight. After writing the interface, we can implement it as
many times as we like, for example, for the image storage cache or anything
else.
interface Square {
kind: ’square’;
size: number;
}
interface Rectangle {
kind: 'rectangle’;
width: number;
height: number;
}
interface Circle {
kind: ’circle’;
radius: number;
}
Now let's look at how we can use the discriminated union in practice. As you
probably already guessed, we will calculate the area of the shapes that are
described using the interfaces.
We could also use if to make the compiler understand which interface we are
currently working with.
Now let's talk about the case when the function can take not only a square,
rectangle, and circle, but also a triangle. At the same time, there is no
corresponding implementation for the triangle. In this case, the compiler will not
throw an error if null and undefined are subtypes. However, if they are full-
fledged types, we will either have to specify number | undefined as the type
returned by the function, or add default in the case of using case, or else in the
case of using if.
The compiler checks the printLabel call. This function takes a single parameter,
which requires that the passed object has a property named label, which would
have a string type. Note that our object has other properties, but the compiler
only checks that it has at least the necessary properties, and their types match the
required ones. In some cases, which we will discuss later, TS does not behave so
leniently.
We can rewrite this example, this time using the interface to reflect the need
for a string-type label property:
interface LabelledValue {
label: string;
}
The LabelledValue interface is the name that can now be used to set the
requirement from the previous example. It still reflects the need for the object to
have a string-type property named label. Note that it is not necessary to
explicitly state that the object that we pass to printLabel implements this
interface, as it would have to be done in other languages. In TS, only the shape
of the object matters. If the object that is passed to the function meets the listed
requirements, then it is considered suitable.
It is worth noting that type checking does not require that the properties go in
a certain order: it is only important that the necessary properties are present and
have the appropriate type.
Optional Properties
Not all interface properties may be required. Some exist only under certain
conditions, or none at all. Such optional properties are often found, for example,
when passing arguments to a function in the form of an object that specifies only
a few properties.
interface SquareConfig {
color?: string;
width?: number;
}
Interfaces with optional properties are written like normal ones, but each
optional property is marked with the symbol ? at the end of the name.
The advantage of optional properties is that you can describe the properties
that may be present, and at the same time prohibit the use of those properties that
are not part of the interface. For example, if we made a mistake when entering
the color name in createSquare, we would get an error message informing us
about it:
interface SquareConfig {
color?: string;
width?: number;
}
Read-Only Properties
Some properties should only be modifiable when the object is created. You can
specify this by adding readonly before its name:
interface Point {
readonly x: number;
readonly y: number;
}
You can create a Point object by assigning an object literal, but after the
assignment, you can no longer change x and y.
In the last line of the example, you can see that even assigning ReadonlyArray
to a regular array is not allowed. However, this restriction can still be
circumvented by using typecasting:
a = ro as number[];
The easiest way to remember when to use readonly and when to use const is to
ask whether this feature is needed for a variable or for an object property. With
variables, const is used, and with properties, readonly is used.
interface SquareConfig {
color?: string;
width?: number;
}
It is very easy to bypass such a check. The easiest way is to use typecasting:
If you are sure that the object can have additional properties that will be used in
some special way, then there is an even better way – to add a string index. If
SquareConfig objects can have color and width properties, as well as any
number of other properties, then the interface can be described as follows:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
We'll discuss indexes later, but for now just note that in this example,
SquareConfig can have any number of properties, and if it's not color or width,
then their type doesn't matter.
The last way to bypass the check for redundant properties – which may seem a
little unexpected – is to assign an object to another variable. Since squareOptions
will not pass the check for redundant properties, the compiler will not throw an
error.
Don't forget that in simple code like the one above, you probably shouldn't try to
bypass this check. For more complex object literals that have methods, or that
have state, you may need to use this technique, but most compiler messages
related to checking for redundant properties indicate real errors. This means that
when you encounter problems that such a check generates (for example, when
passing an object with arguments to a function), you may need to change the
type declarations. In this case, if passing an object that can have both the color
and color properties is acceptable, you need to correct the SquareConfig
definition to reflect this.
Functional Types
Interfaces can describe a wide range of “forms” that JS objects take. In addition
to describing objects with properties, interfaces can also describe function types.
In order to describe a function using an interface, a call signature is added to
it. Such a signature looks like a function description that specifies only the
argument list and the return type. Each parameter in the list must have both a
name and a type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
Once defined, such an interface can be used in the same way as other interfaces.
Now, we will show you how to create a variable of a functional type and assign
it a function.
The parameter names do not have to match in order for the function to pass
type-matching. We, for example, could write the previous example like this:
The function parameters are checked one after the other, and the parameter types
that are in the corresponding positions are compared in pairs. If you don't want to
specify types for arguments, then TS can infer types from the context based on
the fact that the function is assigned to a variable whose type is SearchFunc. In
the following example, the type of the function's return value is also output: this
is done based on the values it returns (false and true). If the function returned
numbers or strings, the compiler would warn during type checking that the type
of the returned value does not match the type specified in the SearchFunc
interface.
Indexed Types
In the same way that interfaces are used to describe functions, you can describe
types so that you can use the index operator with them – for example, like this
a[10] or ageMap [“daniel”]. Indexed types have an index signature that describes
the types that can be used to index an object, as well as the types of values that
this operation returns. Here is an example:
interface StringArray {
[index: number]: string;
}
Here we have a stringArray interface that has an index signature. This signature
says that when stringArray is indexed by a number, a string is returned.
There are only two types of supported index signatures: with strings and with
numbers as an argument. An object can support both types, but the type of value
returned by a numeric index must be a subtype of the one returned by a string
index. This is because when an index operation is applied to an object, JS first
converts the number passed as an index to a string. That is, using the index 100
(number) is the same as using “100” (string), so the types of both indexes must
be consistent.
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
interface NumberDictionary {
[index: string]: number;
length: number; // все хорошо, length — число
name: string; // ошибка, the type of ’name’ is not a subtype of the indexer
}
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
Node-Based Compilation
The simplest TS development environment consists of a simple text editor and a
Node-based TS compiler. Go to the Node website and follow the instructions for
installing Node on the operating system of your choice. Once Node is installed,
you can install TS by simply typing:
This command calls the Node Package Manager (npm) to install TS as a global
module (option-g), which will make it available regardless of which directory we
are currently in. After installing TS, we can display the current version of the
compiler by typing the following:
tsc –v
Now let's create a TS file named hello.ts with the following content:
console.log('hello TypeScript');
From the command line, we can use TS to compile this file into a JS file by
running the following command:
tsc hello.ts
As soon as the TS compiler finishes its work, it will generate a hello file.js in the
current directory. We can run this file using Node by typing:
node hello.js
hello TypeScript
tsc –init
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
}
}
This is a simple JSON file with a single CompilerOptions property that defines
the compilation parameters for the project. The target property specifies the
preferred JS output to generate, and it can be either ES3, ES5, ES6, ES2016,
ES2017, or ESNext. The strict parameter is a flag that includes all parameters of
strict type checking.
It is worth mentioning that TS allows you to use multiple tsconfig files.json in
the catalog structure. This allows different subdirectories to use different
compiler parameters.
Using our tsconfig.json file, we can compile our application by simply typing:
tsc
This command will call the TS compiler using the tsconfig.json file we created
and generate the file hello.js. Virtually any TS source file with the extension .ts
will generate a JS file with the .js extension.
Now, we have successfully created a simple TS development environment
based on Node, with a simple text editor and command line access.
CHAPTER 2
Key Concepts of TS
DOI: 10.1201/9781003203728-2
CLASSES
class Sampler {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let sampler = new Sampler("world");
Inheritance
TS uses the usual approaches of object-oriented programming. Of course, one of
the most fundamental approaches in class-based programming is to create new
classes using inheritance.
Inheritance is another paradigm that is one of the cornerstones of object-
oriented programming. Inheritance means that an object uses another object as
its base type, thereby inheriting all the characteristics of the base object,
including all the properties and functions. Both interfaces and classes can use
inheritance. The interface or class that inherits from it is known as the base
interface or base class, and the interface or class that inherits from it is known as
the derived interface or derived class. TS implements inheritance using the
extends keyword.
Let's look at the following example:
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log('${this.name} moved ${distanceInMeters}m.');
}
}
class Frog extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Jumping...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let jack = new Frog("Froggy the Traveller");
let mrhorse: Animal = new Horse("MrHorse");
jack.move();
mrhorse.move(34);
This example shows the many inheritance features of TS, the same as in other
languages. Here we see the extends keyword used to create a subclass. The
Horse and Frog classes are based on the Animal class and they get access to its
features.
The example shows how to override the methods of the base class using the
methods that are specified in the subclass. The Frog and Horse classes create a
move method that overrides the move method from the Animal class, giving it
functionality specific to each of the classes. Note that although nick is declared
as Animal, its value is Horse, so when nick.move(34) is called, the overridden
method of the Horse class will be called.
Derived classes containing constructor functions must call super (), which will
execute the constructor function of the base class.
Interface Inheritance
The following code can be an example of interface inheritance:
interface IBase {
id: number | undefined;
}
interface IDerivedFromBase extends IBase {
name: string | undefined;
}
class InterfaceInheritanceClass implements IDerivedFromBase {
id: number | undefined;
name: string | undefined;
}
First, we have an interface called IBase, which defines the id property of the
number or undefined type. Our second interface definition, IDerivedFromBase,
inherits (extends) from IBase, and therefore automatically includes the id
property. The IDerivedFromBase interface then defines a name property of the
string or undefined type.
Since the IDerivedFromBase interface inherits from IBase, it actually has two
properties – id and name. Next, we have the definition of the
InterfaceInheritanceClass class, which implements the
IDerivedFromBase interface. Therefore, this class must define both the id and
the name property in order to successfully implement all the properties of the
IDerivedFromBase interface. Although we only have the properties shown in
this example, the same rules apply for functions.
Class Inheritance
Classes can also use inheritance, just like interfaces. Using our IBase and
IDerivedFromBase interface definitions, the code below shows an example of
class inheritance:
interface IFirstInterface {
id : number | undefined;
}
interface ISecondInterface {
name: string | undefined;
}
class MultipleInterfaces
implements IFirstInterface, ISecondInterface {
id: number | undefined;
name: string | undefined;
}
Access Modifiers
Access modifiers allow you to hide the state of an object from external access
and control access to this state. TS has three modifiers: public, protected, and
private.
Public by Default
In our examples, we were able to freely access class members declared in all
classes of the program. If you are familiar with classes in other languages, you
may have noticed that in the examples above, we did not use the word public to
change the visibility of a class member. For example, C# requires each member
to be explicitly marked public for visibility. In TS, however, each class member
will be public by default.
But we can mark the members of the class public explicitly. The Animal class
from the previous section will look like this:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
Private Modifier
When a class member is marked with the private modifier, it cannot be accessed
outside of that class. For example:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
animal = cat;
animal = employee; // error: ’Animal’ and ’Employee’ are not compatible
In this example, we have the classes Animal and Cat, where Cat is a subclass of
Animal. We also have a new Employee class that looks identical to Animal. We
create instances of these classes and try to access each one to see what happens.
Since the private part of Animal and Cat is declared in the same declaration, they
are compatible. However, this does not apply to Employee. When we try to
assign Employee to Animal, we get an error: these types are not compatible.
Even though Employee has a private member named name, this is not the
member we declared in Animal.
Protected Modifier
The protected modifier acts similarly to private, except that members declared
by protected can be accessed in subclasses. For example:
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return 'Hello, my name is ${this.name} and I work in ${this.department}.';
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
Note that we can't use the name member outside of the Person class, but we can
use it inside the Employee subclass method, because Employee comes from
Person.
The constructor can also have the protected modifier. This means that a class
cannot be created outside of the class that contains it, but it can be inherited. For
example:
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in
${this.department}.`;
}
}
Readonly Modifier
You can make properties read-only by using the readonly keyword. Readonly
properties must be initialized when they are declared or in the constructor.
Class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.
interface ICar {
name: string;
age: number;
}
type ReadonlyCar = {
readonly [K in keyof ICar]: ICar[K];
};
This type can be useful, for example, for the state of the Redux application
because the state must be immutable.
We should not be able to change any of its properties once the object has been
created. Type iterator modifiers have become a great addition to the language, as
they make it easy to extend existing types and apply massive changes to all their
properties.
Now, if we declare two models of the car machine: the first object is mutable,
the other is readonly; then try to change their data, we will notice that in the
second case we will have an error.
car.age = 8;
readOnlyCar.age = 10; // Cannot assign to ’age’ because it is a
read-only property
type ReadonlyCar = {
readonly [K in keyof ICar]?: ICar[K];
};
Also, we can specify that all properties are strings, or make each property as a
union of their original type and string through a vertical bar |. There are many
options.
type ReadonlyCar = {
readonly [K in keyof ICar]?: ICar[K] | string;
};
However, with the syntax readonly [K in keyof ICar]: ICar[K]; we can only add
new elements to existing types. We can add the flag readonly or? sign.
If the original type has a property that is optional, for example:
interface ICar {
name: string;
age: number;
color?: string;
}
We can remove the flag not only with? sign. Starting with TS 2.8, it became
possible to add a minus sign (-) before the character that we want to remove.
type ReadonlyCar = {
readonly [K in keyof ICar]-?: ICar[K];
};
Since we have the flexibility with the - sign to remove flags from our types, the
+ sign has also been added to this feature. We can say more clearly what we are
adding and what we are removing.
type ReadonlyCar = {
+readonly [K in keyof ICar]-?: ICar[K];
};
Now everyone is reading this type, and it became clearer that we take the
original ICar interface, remove all optional modifiers -? and add the +readonly
flag for all properties.
Type iterator modifiers are useful if:
Parameter Properties
In our last example, we declared the readonly member name and the constructor
parameter theName in the Octopus class, and assigned theName to name. This is
a very common practice. The parameter properties allow you to create and
initialize members in one place. Here is a further refinement of the previous
Octopus class, using the parameter property:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
Note how we removed theName and shortened the readonly name: string
constructor parameter to create and initialize the name member. We combined
the declaration and assignment in one place.
Parameter properties are declared before a constructor parameter that has an
availability modifier, readonly, or both. Using the private parameter property
declares and initializes the private member; so do public, protected, and
readonly.
Accessors (Getters/Setters)
TS supports getters and setters as a way to intercept accesses to object
properties. This gives you more control over the moment you interact with the
properties of objects.
Let's rewrite a simple class using get and set. First, let's write down an
example without using getters and setters.
class Employee {
fullName: string;
}
class Employee {
private _fullName: string;
To make sure that our access method checks the password, we can modify it and
see that if there is a mismatch, we get a message that we can't modify the worker
object.
Attention: accessors require the installation of code generation in the compiler
according to the ECMAScript 5 standard or higher.
Static Properties
So far, we've only talked about class instance members, the ones that appear in
an object when it's initialized. But we can also create static class members, those
that are visible in the class without creating an instance. In this example, we use
static, since origin is a common value for all objects. Each instance accesses this
value by prefixing it with the class name. Similar to how we add this. to access
instance members, the Grid is used to access static members.
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) /
this.scale;
}
constructor (public scale: number) { }
}
Abstract Classes
Abstract classes are base classes from which others inherit. Their instances
cannot be created directly. Unlike an interface, an abstract class can contain the
implementation details of its members. The abstract keyword is used to define
abstract classes, as well as abstract methods within such classes.
printMeeting(): void {
console.log("The Accounting Department meets each Monday at
10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // okaycreated a reference to an
abstract class
department = new Department(); // error: cannot create an instance
of an abstract class
department = new AccountingDepartment(); // okay, a non-abstract
class was created and assigneddepartment.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on
declared abstract type
Constructors
When you declare a class in TS, you are actually creating multiple declarations
at the same time. The first declaration is of the class instance type.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
In this case, when we say let greeter: Greeter, we use Greeter as the type of
instances of the Greeter class. This is almost a habit of programmers from other
object-oriented programming languages.
We also create another value, which is called a constructor function. This
function is called when we create instances of the class using new. To see how
this looks in practice, let's look at the JavaScript code generated by the compiler
from the example above:
Here let Greeter is assigned a constructor function. When we specify new and
run this function, we get an instance of the class. The constructor function also
contains all the static members of the class. Another way to think about each
class is: there is an instance part and a static part.
Let's change the code a bit to show this difference:
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
class Point {
x: number;
y: number;
}
FUNCTIONS
// Named function
function add (x, y) {
return x + y;
}
/ / Anonymous function
let myAdd = function(x, y) {return x+y; };
Just like in JavaScript, functions can access variables outside of their body.
When this happens, the function is said to “capture” the variables. Although it is
not the task of this article to explain how it works and what the pitfalls of this
technique are, it is important to have a clear understanding of this mechanism in
order to work with JavaScript and TS.
let z = 50;
function addToZ(x, y) {
return x + y + z;
}
Types of Functions
Adding Types to a Function
Let's add the types to the function from the previous simple examples:
You can add types to each parameter, as well as to the function itself, to specify
the type of the return value. TS can infer the type of the return value itself by
analyzing the return statements, so it is often possible not to specify it explicitly.
Now let's describe the full type of this function:
A functional type consists of two parts: the argument types and the return type.
The type of the return value is determined after =>. In the event that the function
does not return any value, void must be specified.
Also in the example above, we changed the names of the parameters passed to
the function. This is done for better code readability. You should always try to
give “talking” or understandable names to the parameters. This will make it
easier for others to read your code, as well as for you when you return to it after
some time.
It is worth noting that only the parameters passed to the function and the
return value determine its type. The captured variables are not included in the
type description. Therefore, they are part of a certain “hidden state” of the
function and are not included in the API.
Inferring Types
Experimenting with the following example, you can see that the TS compiler is
able to deal with types if they are specified in only one-half of the expression:
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// Parameters ’x’ and ’y’ — has "number" type
let myAdd: (baseValue:number, increment:number) => number =
function(x, y) { return x + y; };
This is called contextual typing – a type inference. This feature allows you to
spend less effort on adding types to the program.
There are several places in TS where type inference is used to get information
about types without explicitly specifying it. For example, in this code
let x = 3;
The type of the variable x is output in number. This kind of inference occurs
when initializing variables and members, assigning default values to parameters,
and defining the type of the function's return value.
In most cases, type inference is fairly straightforward. In the following
sections, we will describe several subtleties of this process.
To output the type x in this case, you need to check the type of each element in
the array. In this case, there are two options for the array type: number and null.
The algorithm for finding the best common type checks each candidate type, and
selects the one that is compatible with all the others.
Since the best common type must be chosen from the types provided, there are
cases where the types have a common structure for all, but none of them is the
base for all the others. For example:
Ideally, we would like the zoo type to be output as Animal[] (that is, an array of
objects of the Animal – animal class). But, since there is not a single object in
the array that has the Animal class, the compiler is not able to get such a result.
To fix this, you will have to explicitly specify the type if no object has the base
type for all the others:
If the compiler cannot find the best common type, the output will be the type
of an empty object, that is, {}. Since this type has no members, attempting to use
any of its properties will result in an error. As a result of this inference, you can
still use the object as if its type is unknown, and guarantee type safety in cases
where the object type cannot be found implicitly.
Context Type
In some cases, type inference works in the “other direction” as well. This is
called “context typing.” Contextual typing occurs when you can make a guess
about the type of an expression based on its position. For example:
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.buton); //<- Error
};
To find the type error in this example, the compiler first used the
Window.onmousedown function type to infer the type of the function expression
from the right side of the assignment. After that, it was able to output the type of
the MouseEvent parameter. If this function expression was located where its
type could not be inferred from the context, the type of the MouseEvent
parameter would be any, and the compiler would not throw an error.
If an expression whose type was inferred from the context contains an explicit
type indication, the inferred context type is ignored. That is, if the previous
example were written as:
The explicitly specified parameter type in the function expression will take
precedence over the context type. For this reason, the compiler will not throw an
error, since the context type is not applied.
Contextual typing is used in many cases. Typically, these are arguments when
calling functions, the right-hand side of an assignment, type checks, object
members and array literals, and return statements. Also, the context type is used
as a candidate for the best general type. For example:
There are four candidates for the best overall type: Animal, Rhino, Elephant, and
Snake. The algorithm for finding the best general type is able to choose Animal
from them.
Anonymous Functions
JavaScript also has the concept of anonymous functions. These are functions that
are defined during the operation and do not specify a function name. Consider
the JavaScript code below:
Here, we define a function that has no name and adds two values. Since the
function has no name, it is known as an anonymous function. This anonymous
function is then assigned to a variable named addVar. The addVar variable can
be called as a function with two parameters, and the return value will be the
result of executing an anonymous function. The output of this code will be as
follows:
addVarResult:5
Here, you can see that TS allows anonymous functions in the same way as
JavaScript, but still allows standard type annotations. The result of this code is as
follows:
addFunctionResult: 5
Type Compatibility
Type compatibility in TS is based on structural typing. Structural typing is a way
to identify type relationships based solely on the composition of their members.
This approach differs from nominative typing. Let's look at the following code:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// Everything fits, since the structural type system
p = new Person is used();
In languages like C# and Java, where the nominative type system is used, similar
code would lead to an error, since the Person class is not explicitly described as
implementing the Named interface.
The TS structural type system was designed with the way JavaScript code is
usually written in mind. Since JavaScript makes extensive use of anonymous
objects, such as function expressions and object literals, it is much more natural
to describe their relationships using a structural system rather than a nominative
one.
The TS type system allows for certain operations that cannot be said to be safe
at compile time. When a type system has this property, it is said that it is not
“reliable.” The places where TS allows unreliable behavior have been carefully
considered, and in this chapter, we will explain where this happens and for what
reason it was allowed.
The basic rule of the TS type system is that x is compatible with y if y has at
least the same members as x. For example:
Note that y has an additional location property, but this does not result in an
error. When checking for compatibility, only members of the target type (in this
case, Named) are considered.
The comparison process is performed recursively, affecting the types of all
members and sub-members.
In JavaScript, all parameters are optional, and users can skip them if necessary.
In such cases, the value of the missing parameters is assumed to be undefined. In
TS, you can also achieve this: to do this, at the end of the parameter that you
want to make optional,? is added. For example, we want to make lastName
optional from the previous example:
All optional parameters must come after the required ones. If the first parameter
(firstName) had to be made optional instead of lastName, then the order of the
parameters in the function would have to be changed so that firstName would be
the last.
TS also allows you to specify a value for a parameter that it will accept if the
user skips it or passes undefined. These parameters are called default parameters,
or simply default parameters. Let's take the previous example and set the default
value for lastName to “Smith.”
The default parameters that follow all the required parameters are considered
optional. Just like the optional ones, you can skip them when calling the
function. This means that the types of optional parameters and default
parameters that are at the end will be compatible, so this function …
And this
function buildName(firstName: string, lastName = "Smith") {
// …
}
will have the same type (firstName: string, lastName?: string) = > string. The
default value for the lastName parameter in the function type description
disappears, leaving only the fact that the last parameter is optional.
Unlike simple optional parameters, the default parameters do not have to be
placed after the required parameters. If the default parameter is followed by a
mandatory parameter, you will have to explicitly pass undefined to set the
default value. For example, the last example can be rewritten using only the
default parameter for firstName:
Rest Parameters
Mandatory, optional, and default parameters have one thing in common – they
describe one parameter at a time. In some cases, you need to work with several
parameters, treating them as a group; and sometimes it is not known in advance
how many parameters the function will take. In JavaScript, you can work with
arguments directly using the arguments variable, which is available inside any
function.
In TS, you can assemble arguments into a single variable:
this Keyword
Learning how to use this keyword correctly in JavaScript is something of a rite
of passage for developers. Since TS is a superset of JavaScript, TS programmers
also need to understand how to use this and how to notice when this is used
incorrectly. Fortunately, TS allows you to detect the incorrect use of this with a
few tricks.
As a rule, an object method needs access to the information stored in the
object in order to perform any actions with it (in accordance with the purpose of
the method).
For example, the code inside user. SayHi() may need the user name that is
stored in the user object. To access information inside an object, a method can
use the this keyword. The value of this is the “before the dot” object that was
used to call the method.
let user = {
name: "John",
age: 30,
SayHi() {
// this is the " current object"
alert(this.name);
}
};
Here, during the execution of the user.SayHi () code, the value of this will be
user (a reference to the user object).
Technically, it is also possible to access an object without the this keyword by
referring to it through an external variable (which stores a reference to this
object):
let user = {
name: "John",
age: 30,
SayHi() {
alert(user.name); / / use the "user" variable instead of the "this" keyword"
}
};
But such code will be unreliable. If we decide to copy the reference to the user
object to another variable, for example, admin = user, and overwrite the user
variable with something else, then the wrong object will be accessed when
calling the method from admin.
This is shown below:
let user = {
name: "John",
age: 30,
SayHi() {
alert( user.name ); / / will result in an error
}
};
let admin = user;
user = null; // reset the variable for clarity, now it does not store a reference to
the object.
admin.SayHi (); / / Error! Inside SayHi (), the user is used, which no longer
references the object!
If we use this.name instead of user.name inside the alert, then this code will
work.
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
//ATTENTION: the line below is an arrow function that captures the value of
’this’ from this place
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
this Parameters
Unfortunately, the type of the expression this. suits[pickedSuit] is still any, since
this is taken from a function expression inside an object literal. To fix this, you
can explicitly specify this as a parameter. The this parameter is a “fake”
parameter that comes first in the list of function parameters:
We will add several interfaces to the previous example: Card and Deck, to make
the types more understandable and easy to reuse:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// ATTENTION: Now the function explicitly indicates that it should be called
on an object of the Deck type
createCardPicker: function(this: Deck) {
return () => {
x
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13};
}
}
}
The compiler now knows that the createCardPicker function expects to be called
on an object with the Deck type. This means that the type of this value is now
Deck, not any, and the –noImplicitThis flag will not throw errors.
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void means that addClickListener assumes that the onclick function does
not require this. Secondly, the code that is called must also be accompanied by
the this parameter:
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// this is used here! The function will crash at runtime!
this.info = e.message;
};
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
When this is specified, it explicitly reflects the fact that onClickBad must be
called on an instance of the Handler class. Now TS will detect that
addClickListener requires a function with this: void. To fix this error, change the
this type:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// you can't use the this variable here, because it has the void type!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
Since the onClickGood function specifies that the type of this is void, it can be
passed to addClickListener. Of course, this also means that it can no longer be
used in it this.info. But if you need both, you will have to use the arrow function:
class Handler {
info: string;
onClickGood = (e: Event) => {this.info = e.message}
}
This will work because arrow functions do not capture this from the context in
which they are executed, and they can be freely passed where a function with
this: void is expected. The disadvantage of this solution is that for each Handler
object, its own arrow function will be created. Methods, on the other hand, are
created only once, are associated with the Handler prototype, and are common to
all objects of this class.
Overloads
TS supports function overloading, meaning we can define multiple versions of a
function that will have the same name, but different parameter types or different
number of parameters. For overloading, we first define all versions of the
function that will not have any logic. And then we define a version of the
function with a common signature that fits all the previously defined options.
And in this general version, we already define the specific logic of the function.
For example, we need to combine two values, but if they represent strings,
then just concatenate them, and if numbers, then add them. Then we could use
the following function:
The first version of the add function takes two strings and returns a string, the
second version takes two numbers and returns a number. Common to them is a
function that takes parameters of type any and returns a result of type any as
well.
But if we applied the same function to boolean values:
Then we would get an error, since two versions of the function allow you to take
either two strings or two numbers as parameters. And in this case, we would
need to add another version of the function for boolean values:
In this example, the pickCard function returns two different things, depending on
what was passed to it. If the user has passed an object that represents a deck of
cards, the function will select one of the cards. If the user passes the card, the
function will determine which card he chose. But how do you describe this
behavior using a type system?
You need to specify several types for one function, creating a list of overloads.
The compiler will use this list for checks when calling the function. Let's create a
list of overloads that describes what the pickCard function accepts and what it
returns.
By changing the code in this way, we get the ability to call the pickCard
function, performing a type check.
In order to select the correct type check, the compiler performs actions similar
to those in JavaScript. It looks through the list of overloads, starting with the first
element, and matches the function parameters. If the parameters are appropriate,
the compiler selects this overload as the correct one. Therefore, as a rule,
function overloads are ordered from the most specific to the least specific.
Note that the code section function pickCard(x): any is not included in the list
of overloads; there are only two elements in this list, one of which takes an
object and the other a number. Calling pickCard with any other types of
parameters will result in an error.
GENERICS
Of course, in TS, we can specify any so that the function can be used for any
data type:
We can say that this function is a generalization, since it can work with any type
of data. But in this case, we have absolutely no information about the type of the
returned value. For example, we pass a number as an arg, but we don't know
anything about the specific type that the function will return, since it is any.
When using generalizations, we can explicitly specify the type of the
argument and thus, within the framework of our example, determine the type of
the return value. To do this, we need to use a special type variable that only
works with data types, not their values:
When calling the function in angle brackets < >, we explicitly specified the data
type that the function will work with.
The second method is not to specify the data type in angle brackets, but
simply pass the variable as an argument to the function. In this case, TS uses a
mechanism called automatic type inference, according to the value passed as an
argument.
The second method, on the one hand, makes the code shorter and easier to read,
but on the other hand, it can lead to errors. After all, in this case, you can pass a
value to the function that is not expected there, and this can lead to an error at
runtime.
What if you need to output the length of the arg argument to the console every
time the function is called? You may be tempted to write like this:
function loggingIdentity<t>(arg: T): T {
console.log(arg.length); // Error: T has no.length property
return arg;
}
</t>
If you do this, the compiler will throw an error telling you what is being used the
.length of the arg object, although it was never specified that the object has such
a property. Earlier it was said that a type variable means absolutely any type, so
a number that does not have a property could also be passed to the function
.length.
Let's assume that the function should actually work with arrays of T objects,
and not with the T objects themselves directly. Since it will deal with arrays,
they must have a property .length. You can describe it as if we are creating an
array:
You may have already encountered this kind of type writing in other languages.
In the next section, we'll discuss how to create your own custom types like
Array<T>.
Generalized Types
In the previous sections, we created a generalized identity function that worked
with different types. In this section, we will discuss how to describe the type of
such a function and how to create generalized interfaces.
Defining a generic function type is very similar to defining a type for a regular
function. The only difference is that you first need to specify the type of
parameters to be passed and the return value, just like when creating a
generalized function:
function
identity<t>(arg: T): T {
return arg;
}
A different name could be used for the standard parameter, but it is only
important that the number of standard parameters and how they are used are
consistent.
You can also write a generic type as a call signature on the type of an object
literal:
This brings us to the description of the first generalized interface. Let's take the
object literal from the previous example and turn it into an interface:
interface GenericIdentityFn {
<t>(arg: T): T;
}
Moreover, we can specify a generic type for the entire interface, which will
make this parameter available to all its methods. After that, we can use a
generalized version of the interface, specifying a specific data type (for example,
Dictionary<string> instead of Dictionary):
interface GenericIdentityFn<t> {
(arg: T): T;
}
Note that the example was transformed into something completely different.
Instead of describing a generalized function, it is now an ordinary, non-
generalized function that is part of a generalized type. When using
GenericIdentityFn, you will now have to specify the appropriate type argument
(in this case, number), thus fixing the types that the corresponding function will
use. Understanding when a type parameter should be added to the call signature
and when to the interface itself is useful when describing which aspects of the
type are generalized.
In addition to generic interfaces, you can also create generic classes. Note that
you cannot create generic enumerations and namespaces.
Generalized Classes
Generalized classes have the same form as generalized interfaces. They have a
list of typical parameters in angle brackets (< >) after the class name.
class GenericNumber<t> {
zeroValue: T;
add: (x: T, y: T) => T;
}
This is a fairly literal use of the GenericNumber type (lit. a generalized number),
but you can see that nothing prevents you from using other types with it, except
number. For example, you can use the string type or more complex objects.
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
</string>
Just as with interfaces, passing a type parameter to the class itself establishes that
all its properties will work with the same type.
As mentioned in Chapter 2, a class has two types: the static part type and the
instance type. Generic types are such only in relation to the instance type, but not
to the static part type. Therefore, static class members cannot use typical class
parameters.
Limitations of Generalizations
As you remember from the previous examples, in some cases you need to create
a generalized function that works with a certain set of types, for which you know
what capabilities they have. In the example with loggingIdentity, you needed to
access the property .the length of the arg object, but the compiler couldn't be sure
that any type would have such a property, so it warned about it.
Instead of working with any possible type, we would like to create a constraint
so that the function works with all types that have a property .length. If a type
has this property, it can be used, but it must have at least this property.
To implement this, we will create an interface that describes such a restriction.
Creating an interface with a single property. length, and use it with the extend
keyword to denote a constraint:
interface Lengthwise {
length: number;
}
Since the generalized function now has a constraint, it will not be able to work
with any type:
Instead, it needs to pass values of those types that have all the necessary
properties:
function
create <T>(C:
{ new(): T;
}): T {
return new c();
}
Class SomeClass {}
var obj = create(SomeClass);
console. log(obj.constructor.name);
// shows SomeClass
The following example shows how to impose restrictions on the types of classes
created using the class factory:
class
BeeKeeper
{
hasMask: boolean = false;
}
class Zookeper {
nametag: string = "tag";
}
class Animal {
numlegs : number;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<TAnimal extends Animal>(c: new () =>
TAnimal): Tanimal
return new c();
}
console.log(createInstance(Lion).keeper.nametag); shows "tag"
console.log(createInstance(Bee).keeper.hasMask); // shows "false"
new Keyword
To create a new object in the generalization code, we need to specify that the
generalized type T has a constructor. This means that instead of the type: T
parameter, we need to specify type: {new(): T;}. For example, the following
generalized interface will not work:
function UserFactory<T>(): T {
return new T(); // compilation error
}
ENUMS
Enums allow us to define a set of named numeric constants and are defined
using the enum keyword. Enumerations are subtypes of the primitive number
type.
enum Direction {
Up = 1,
Down,
Left,
Right
}
if the value is not explicitly defined and the value of the previous
member is constant. In this case, the value will be equal to the numeric
value of the previous term plus 1. The exception is only the first
element. If the value is not specified, it becomes 0;
if the enumeration element is defined using a constant expression. Such
expressions are a type of TS expression that can be evaluated at compile
time.
In all other cases, the enumeration member is considered evaluated and its value
is evaluated at the time of program execution.
enum FileAccess {
// constant elements
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// evaluated elements
G = "123".length
}
Enums are valid objects that exist at runtime. One reason for this is the ability to
support reverse mapping from enum values to enum names.
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[Enum.A]; // "A"
compiled in:
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[Enum.A]; // "A"
In the generated code, the enumeration is compiled into an object that stores the
forward (name -> value) and reverse (value -> name) mappings. References to
enum elements are always executed as property accesses and are never
embedded. In many cases, this is the right solution. However, sometimes the
requirements are tougher. To avoid paying the cost of additional generated code
and indirect access when accessing enumeration values, you can use constant
enumerations. Constant enumerations are defined using the const modifier
preceding the enum keyword.
Constant Enums
In order not to generate additional code and unnecessary references to access the
values of the enumerations, you can use constant enumerations. They are defined
using the const keyword, which is placed before enum. Using constant
enumerations increases the performance of the code, because instead of the
enumeration element, the constant associated with it is used.
const enum
Directions
{
Up,
Down,
Left,
Right
}
let directions =
[Directions.Up, Directions.Down, Directions.Left, Directions.Right]
var Directions;
(function (Directions) {
Directions[Directions["Up"] = 0] = "Up";
Directions[Directions["Down"] = 1] = "Down";
Directions[Directions["Left"] = 2] = "Left";
Directions[Directions["Right"] = 3] = "Right";
})(Directions || (Directions = {}));
Now, let's see what the someFunc function will look like after compilation:
function
someFunc(op)
{
switch (op) {
case 0:
break;
case 1:
break;
}
}
As you can see, the corresponding constants were substituted instead of the
enumeration elements.
Declare Enums
declare enum Enum {
A = 1,
B,
C = 2
}
Such enumerations are defined using the declare keyword. They are used to
describe the form of existing enumerations.
The difference between such enumerations is that if an element in such an
enumeration does not have an initializer, then it is considered evaluated. In
normal enumerations, the opposite is true.
The compiler will not generate code for such enumerations. This can be useful
when using third-party libraries (for example, jQuery, in which a certain object
is defined (for example, $) when you need information about an object, but you
don't need to generate code.
SYMBOLS
ECMAScript 2015 introduced the symbol type – a primitive data type similar to
number and string.
Values of the symbol type are created by calling the Symbol constructor.
let obj = {
[sym]: "value"
};
console.log(obj[sym]); // "value"
class C {
[getClassNameSymbol](){
return "C";
}
}
Predefined Characters
In addition to user-defined characters, there are predefined built-in characters.
Embedded characters are needed to reflect the internal behavior of the language.
List of predefined characters:
DOI: 10.1201/9781003203728-3
Modules are executed not in the global scope but in their own scope. This means
that variables, functions, classes, etc. declared in the module are not visible
outside the module, except when they are explicitly exported using one of the
export forms. Also, to use a variable, function, class, interface, etc. exported
from another module, you need to import them using one of the import forms.
WHAT IS NAMESPACE?
namespace Identifier {}
namespace NamespaceIdentifier {
class PrivateClassIdentifier {}
export class PublicClassIdentifier {}
}
var NamespaceIdentifier;
(function (NamespaceIdentifier) {
class PrivateClassIdentifier {}
class PublicClassIdentifier {}
NamespaceIdentifier.PublicClassIdentifier =
PublicClassIdentifier;
})(NamespaceIdentifier || (NamespaceIdentifier = {}));
It's also worth adding that namespace is a global declaration. This literally means
that a namespace declared as global does not need to be exported or imported
and a reference to it is available anywhere in the program.
WHAT IS MODULE?
// declaration.ts file
export type T1 = {};
export class T2 {}
export class T3 {}
// index.ts file
In addition, you can even declare a namespace using the export keyword. This
will limit its global scope and its use in other files will only be possible after
explicit import.
// declaration.ts file
It should be noted that you should export a namespace only when it is declared
in the body of another namespace, but you need to get to it from the program.
namespace NS1 {
export namespace NS2 {
export class T1 {}
}
}
Export
Any declaration (by a variable, function, class, type alias, or interface) can be
exported by adding the export keyword.
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;
Export Validation
Export definitions are useful when you need to rename the exported elements.
Then, the above example can be rewritten as follows:
Reexport
Modules often extend other modules. At the same time, they themselves provide
access to some of the functions of the source modules. Reexport does not
perform a local import and does not create a local variable.
ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
AllValidators.ts
export * from ". / StringValidator"; / / exports the
’StringValidator’ interface
export * from "./LettersOnlyValidator"; / / exports the
’LettersOnlyValidator’ class
export * from "./ZipCodeValidator"; / / exports the
’ZipCodeValidator’class
Import
Importing is almost as easy as exporting. To import an exported ad, use one of
the import forms below:
Importing the Entire Module into a Single Variable, and Using It to Access
the Exported Module Elements
Default Export
Each module can contain a default export. The default export is highlighted with
the default keyword, and there can only be one such statement in a module. To
import an export, a separate form of the import statement is used by default.
The default export can be very useful. For example, a library like Jquery can
export jQuery or $ by default, which we probably also import under the name $
or jQuery.
JQuery.d.ts
declare let $: JQuery;
export default $;
App.ts
import $from "JQuery";
$("button.continue").html ("Next Step…" ) ;
ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 &&
ZipCodeValidator.numberRegexp.test(s);
}
}
Test.ts
import validator from "./ZipCodeValidator";
let myValidator = new validator();
StaticZipCodeValidator.ts
const numberRegexp = /^[0-9]+$/;
Test.ts
import validate from "./StaticZipCodeValidator";
let strings = ["Hello", "98052", "101"];
OneTwoThree.ts
export default "123";
Log.ts
import num from "./OneTwoThree";
console.log(num); // "123"
CommonJS and AMD have the concept of an exports object that contains all the
module exports.
They also support replacing the exports object with a single user object. The
default export is intended to replace this functionality. Both approaches,
however, are incompatible. TypeScript supports the export = construct, which
can be used to model the familiar way CommonJS and AMD work.
The export = construct defines a single object to export from the module. This
can be a class, interface, namespace, function, or enumeration.
To import a module exported with export =, the TypeScript-specific construct
import let = require("module") must be used.
ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
Test.ts
import zip = require("./ZipCodeValidator");
// A few test cases
let strings = ["Hello", "4546", " 101"];
// Validators
let validator = new postcode();
SimpleModule.ts
import m = require("mod");
export let t = m.something + 1;
UMD SimpleModule.js
(function (factory) {
if (typeof module === "object" && typeof module.exports ===
"object") {
var v = factory(require, exports); if (v !== undefined)
module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
SimpleModule.js System
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
LettersOnlyValidator.ts
import { StringValidator } from "./Validation";
ZipCodeValidator.ts
import { StringValidator } from "./Validation";
Test.ts
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// Validators
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
if (needZipValidation) {
let ZipCodeValidator: typeof Zip =
require("./ZipCodeValidator");
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}
Example: Dynamic loading of modules in System.js
declare const System: any;
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip)
=> {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}
EXTERNAL MODULES
In Node.js, most tasks are performed by loading one or more modules. We could
define each module in its own file .d.ts in top-level export declarations, but it is
much more convenient to put the definitions of all modules in one common .d.ts
file. To do this, use a construct similar to external namespaces. It uses the
module keyword and the quoted module name, which will be available for
further import. For example:
declarations.d.ts
declare module "hot-new-module";
All imported elements of such a module will have the type any.
You can now import elements that match"*! text " or " json!*".
UMD MODULES
Some libraries are designed to be used with many module loaders or no loaders
at all (global variables). They are called UMD or Isomorphic modules. You can
connect such libraries either by importing them or as a global variable. For
example:
math-lib.d.ts
export const isPrime(x: number): boolean;
export as namespace mathLib;
You can connect this library inside the module by importing it:
mathLib.isPrime(2);
STRUCTURING MODULES
Export as Close to the Top Level as Possible
The less problems the module users have with using the exported elements, the
better. Adding nesting levels makes the module more cumbersome, so you need
to think carefully about its structure.
Exporting from a namespace module is just an example of adding an extra
level of nesting. Although namespaces can be useful, they add another layer of
abstraction to modules, which can lead to problems for users very soon and is
usually not necessary.
Static methods of exported classes cause similar problems, since the class
itself adds a level of nesting. It is acceptable to do this if you know exactly what
you are doing, and the introduction of an additional level of nesting will add
expressiveness and clearly reflect the purpose of the module. Otherwise, we
recommend using the helper function.
MyClass.ts
export default class SomeType {
constructor() { ... }
}
MyFunc.ts
export default function getThing() { return "thing"; }
Consumer.ts
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());
This approach is optimal for users of the module. They can give your type the
most convenient name for them (t in this case) and will be spared the
unnecessary use of “through the dot” to search for your objects.
Consumer.ts
Risk Indicators
The following is a list of warning signs regarding module structuring. Once
again, make sure that you are not trying to create namespaces for your external
modules, if any of the following statements apply to your situation:
Loading Modules
Past topics have covered how to define and import modules in TypeScript.
However, native browsers do not yet support working with modules. Maybe this
feature will be implemented in a beautiful browser of the future, but at the
moment you need to use special tools to load modules, which are called loaders.
In this topic, we will look at loading modules using the SystemJS loader.
First of all, it is worth noting that loading from the server is done via AJAX,
so such a TypeScript application must be hosted on a web server. That is, we
will not be able to simply throw the page into a web browser, as, for example, it
was in the first topics. Therefore, first of all, you need to decide on the web
server. The web server can be anything. In this case, we will use the most
democratic option - Node.js. To do this, we will only need to install it on your
computer Node.js.
First, we will determine the folder on the hard disk where the project will be
located. Let's say it will be folder C:\typescript. And first of all, we will define
the server file in it. Let it be called server.js and will have the following code:
This is the most primitive server that gives the user static files. The
http.createServer function is used to create a server, and the fs.ReadFile ()
function is used to read and send files. The server will start at
http://localhost:3000/. For testing purposes, nothing else is needed in the
pritsnip. But again instead of node.js this can be any other server technology-
php, asp.net, python, etc.
Let's define the app directory in the project, where the TypeScript files will
actually be located. Add the file devices.ts to this directory, which will represent
the simplest module:
export interface Device{
name: string;
}
This file loads the devices module and uses the types defined in this module.
Now, in the root folder of the project, we will define the web page of our
application-the file index.html:
<html>
<head>
<meta charset="utf-8" />
<title>TypeScript Modules</title>
</head>
<body>
<h1>TypeScript Modules</h1>
<div id="content"></div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.21.0/system.js"></script>
<script>
SystemJS.config({
baseURL: "app",
packages: {
"/": { defaultExtension: "js" }
}
});
System.import("app.js");
</script>
</body>
</html>
First of all, SystemJS is loaded from the CDN at the bottom of the page. Next,
the bootloader is configured using the SystemJS.config () function, so that it uses
our files. First of all, using the baseUrl: “app” parameter, that files will be
located in the app folder (where we currently have typescript files).
Since in the end we will compile TypeScript files in JavaScript (since only
JavaScript is supported by the browser), then in this case we will only work with
js files. To do this, we define the packages: {“/”: {defaultExtension: “js”}}
parameter. “defaultExtension” indicates the extension that will be added to the
modules.
After that, the main application file is imported - in our case app.js (to which
app.ts is compiled): System. import(“app.js”).
Module Resolution
Module resolution is the process used by the compiler to find out what the
import command refers to. Consider the following statement: import {a} from
“ModuleA.” To check the correctness of the use of a, the compiler must know
exactly what this element is, for which it is necessary to check the corresponding
definition-ModuleA.
Relative imports are allowed relative to the imported file and cannot be resolved
by declaring an external module. Relative imports are best used for your
modules, which are guaranteed to be in the specified location during program
execution.
Nonrelative imports can be resolved relative to baseUrl or by using path
mapping, which will be described below. It can also be resolved by external
module declarations. Use nonrelative paths when importing any external
dependencies.
For nonrelative module imports, the compiler will search the directory tree for a
suitable definition file, starting with the directory containing the importing file.
For example:
Nonrelative import from ModuleB, such as import {b} from "ModuleB",
located in the source code file /root/src/folder/A.ts, will search for "ModuleB" in
the following locations:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json (if it defines the
"main" property)
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json (if it defines the "main"
property)
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json (if it defines the "main"
property)
/node_modules/moduleB/index.js
Base URL
The baseUrl is often used in applications that use the AMD module loader,
where modules are dynamically “deployed” in a single directory. The source
files of these modules can be located in different places, but the build script will
put them all in the same directory.
Setting baseUrl tells the compiler where to look for modules. All module
import commands with nonrelative names are considered relative baseUrl.
The baseUrl value is defined as one of:
Note that setting baseUrl does not affect the relative module import commands,
as they are always resolved relative to the importing files.
Path Mapping
Sometimes the modules are not directly under the baseUrl. For example, the
“jquery” module import command at runtime will be converted to
“node_modules\jquery\dist\jquery.slim.min.js”. Loaders use the path mapping
configuration to dynamically match module names and corresponding files, see
the RequireJS and SystemJS documentation.
The TypeScript compiler supports declaring such mappings in the “paths”
property of the tsconfig.json file. Here is an example of how you can specify the
“paths” property for jquery.
{
"compilerOptions": {
"paths": {
"jquery": ["node_modules/jquery/dist/jquery.d.ts"]
}
}
The “paths” property allows you to use more complex mapping methods,
including multiple backup paths. Let's look at a configuration in which only
some modules are available in one location, while the rest are located in another.
When you build, all these modules will be placed in one place. The project
diagram may look like this:
projectRoot
├── folder1
│ ├── file1.ts (импортирует ’folder1/file2’ u ’folder2/file3’)
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
The corresponding tsconfig.the json will look like this:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}
So we tell the compiler that for each module whose import statement matches
the “ *” pattern (i.e. any values), it must search in two places:
“*”: meaning the same name without changes, so match <moduleName> =>
<baseUrl>\<moduleName>
“generated\*” meaning the module name with the added prefix “generated,”
so match <moduleName> => <baseUrl>\generated\<moduleName>
Following this logic, the compiler will try to resolve the specified import
instructions as follows:
import ‘folder1/file2’
there is a match to the ‘*’ pattern, which covers the entire module name;
try the first replacement in the list: ‘*’ -> folder1/file2;
the result of the replacement is a relative name, we connect it with
baseUrl -> projectRoot/folder1/file2. ts;
The file exists. Done.
import ‘folder2/file3’
there is a match to the ‘*’ pattern, which covers the entire module name;
try the first replacement in the list: ‘*’ -> folder2/file3 the
result of the replacement is a relative name, connect it to baseUrl ->
projectRoot/folder2/file3. ts.
The file does not exist, go to the next replacement
the second replacement is ‘generated/*’ -> generated/folder2/file3 the
result of the replacement is a relative name, we connect it with baseUrl -
> projectRoot/generated/folder2/file3. ts.
The file is done.
src
└── views
└── view1.ts (imports ’./template1’)
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports ’./view2’)
src / views contain files with custom code for UI elements. The files in generated
/ templates contain the UI template binding code that is automatically generated
by the template generator as part of the build. In one of the build steps, the files
from /src/views and /generated/templates/views will be copied to the same
directories in the output structure of the project. The view at runtime expects its
template to be nearby, and it can be imported using the relative path ".
/template".
To indicate this relationship to the compiler, use “rootDirs.” “rootDirs”
defines a list of root directories (roots) whose contents need to be merged
dynamically. Continuing our example, the tsconfig file.the json should look like
this:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
tsc –traceResolution
Results:
TS Runtime
DOI: 10.1201/9781003203728-4
WHAT IS RUNTIME?
NODE.JS
Node is a runtime environment that allows you to write server-side JavaScript
(JS) code. It became very widely distributed after its release in 2011. With the
growth of the code base, writing server-side JS code can be difficult due to the
nature of the JS language: dynamic and weakly typed.
Node.js is not a separate programming language but a platform for using JS on
the server side. If we talk about the language, then both the frontend and the
backend use the same JS. The only difference is in the set of APIs that
frontenders and backenders use.
Browser-based JS uses Web APIs that provide access to the DOM and user
interface of pages and web applications. Server-side JS uses APIs that provide
access to the application file system, http requests, and streams.
That is, Node.js is a technology for using JS on the backend. The features and
prospects of the development of the JS language can be found in the
corresponding article, and here we are talking about one of the technologies of
this language.
Both are browser-based JS and Node.js runs in the V8 runtime. This engine
uses your JS code, and converts it to faster machine code. Machine code is low –
level code that a computer can run without having to interpret it first.
Using Node.js implements the “JavaScript for everything” paradigm. It
involves using a single programming language to develop web applications
instead of using different languages to work on the frontend and backend.
Most often, this platform is used to create web services that require intensive
information exchange with users, including the implementation of chats,
collaboration systems, social networks, etc. Many programs created on Node.js,
consists of server and client parts.
Developers switching to JS from other languages often complain about the lack
of powerful static typing, but TypeScript (TS) allows you to eliminate this
drawback.
Technically speaking, TS is a superset of JS, and that means that all JS code is
correct TS code.
Why Node.js?
The most significant advantage is the non-blocking I/O model. This system is
event-driven and works asynchronously, lining up the queue by priority. When
thousands of people connect to the server at the same time, it is easier for it to
cope with the load, since there is no need to create a separate thread for each
connection. Proper allocation of resources helps to withstand a larger number of
connections.
WHAT IS DENO?
Module System
Forget about package.json and node_modules. When importing source files, you
can specify either a relative or absolute path, or their full URL:
By default, all source files are cached. You can use the –reload argument to
update dependencies. It works like the F5 key in the browser.
TS support out of the box
TS is supported in Deno by default. That's it. Without any “but.” Without
configurations.
Installation of Deno
Let's get down to business, and start with the installation, we will analyze what,
where, how, and why it is installed.
Using Shell (Mac, Linux):
Here I should warn you that installing with the help of package managers carries
a danger in the form of an incomplete complete removal and update mechanism.
It is best to control the versions and paths where it will be installed.
Also, to upgrade to the current version, the Deno upgrade command is
provided.
Well, now let's analyze the installer itself using the example of installation in
Windows.
Installation in Windows
Almost everything in Deno is based on GitHub, and the installation will be made
from it, and the latest release will be taken from it.
In the future, to ensure the operation of Deno as a whole, the following files
and directories will be created along the path {username}\AppData\Local\deno:
deps: This directory will contain all the dependencies that were ever
downloaded, grouped by the download path, and also next to each file
will be a separate metadata file, which specifies the response headers,
from the server that was accessed during the build, and, of course, the
URL itself. This is actually the repository of your dependencies, where
Deno will first turn during the launch of your project. They can also be
reloaded if necessary by adding the-r flag to the Deno run command.
For those who are worried about lock files and cross-dependencies, this
will be described in detail in the section “Deno functionality.”
gen: Your js-compiled TS files will be located here. This, as you
probably already understand, provides a quick start of the application
after the first build.
Compiled projects will be located here too. For example, at the path
gen/file/C/dev/my_first_project
deno_history.txt: This file contains a short-term history of entering
commands in Deno in console mode.
lib.deno.d.ts, lib.deno_runtime.d.ts: This file contains the typings of
Deno core itself. You will need to know about them when you try to
play with different versions of Deno.
Internal Part
Safety
Deno is safe by default. For comparison, Node.js has full access to your file
system and network.
To run the program without permissions, just run the command:
environment
network
file system
running child processes
If you get tired of typing all the flags each time, you can write it in the bash
script or use the-A (– allow-all) flag, which I highly recommend. Or you will
like Drake-an analog of make only for Deno.
Also, you can install the Deno program by using Deno install with the
necessary permissions. After installation, your program will be available
globally, as it will be in $PATH.
tsconfig.json
Deno has a preset config by default, but you can set your own custom config, as
in normal applications, with the exception of some options. I prefer to use
decorators in TS, so there is no way without a custom config. Here is an example
of running with a custom config:
deno run -c tsconfig.json main.ts
Deno Modules
One of the main major differences between Deno and Node.js is that Deno uses
the official ECMAScript module standard, not the deprecated CommonJS. In
Node.js ES modules only appeared at the end of 2019, with version 13.2.0, but
even then, support remained immature, and it is still enabled by the controversial
extension. mjs. Deno breaks out of the past, using modern web standards for its
modular system.
As you probably noticed, unlike your usual TS code, you need to specify the
file extension in the paths. This has a positive effect on the build speed of your
applications. As Ryan Dahl has already noticed, not using extensions when
required was an N1 design error. Node.js.
The modules can also be divided into three categories: Core, Std, X.
Deno.core
Here is the code that is necessary to ensure the work with the Rust backend.
Deno X
This category includes everything. What was not developed by the main Deno
team, as well as any import of third-party code into your application.
https://deno.land/x – is only a link shortener. And you can add your lib to this
registry by sending a pull request.
JSPM, Pika.dev – sites designed primarily for converting any npm package to
an ES module. For example so:
Package Manager
There has been a radical rethink regarding how package management works in
Deno. Instead of relying on a central repository, it is decentralized. Anyone can
place a package anywhere. There are advantages and disadvantages to using a
centralized repository like npm, and this aspect of Deno is sure to be the most
controversial.
No more package.json, now it is accepted to use deps.ts, in which you can
describe the necessary imports as follows:
You can also use the-importmap (-unstable) flag, which will allow you to
describe all imports in a single json file:
{
"imports": {
"moment": "./moment/moment.ts",
"moment/": "./moment/",
"lodash": "./lodash/lodash.ts",
"lodash/": "./lodash/",
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js": "./vue.ts" },
"scopes": {
"scope/": {
"moment": "./scoped_moment.ts"
}
}
}
It does not support std modules, but, nevertheless, it can work with the following
schemes: file:, http:, https:.
Lock File
Deno can store and verify the integrity of subresources of modules using a small
JSON file. Using the – lock = lock flag.json to enable and specify file validation.
To update or create a file, use – lock = lock.json – lock-write.
Web Standards
Everything that works in Deno works the same way as in the browser by default,
and Deno from the very beginning tries to ensure that there are no strange errors
for Deno users. What I mean by that:
> reverse=a=>a.sort(n=>1)
> reverse([1,2,3])
- node
(10.13.0): [3,2,1]
- deno (0.4.0)
[1,2,3]
- chrome (74.0)
[1,2,3]
- other browsers:
[1,2,3]
Debugging
Deno has built-in debugging, but at the time of this writing, the Visual Studio
Code extension does not support it. To debug manually, you need to do the
following:
Open in Chrome chrome:/ / inspect, see Target Deno, and click Inspect. This will
give you debugging in the browser.
Deno has a built-in file monitoring mechanism using the Rust notification
library via the Deno.watchFs () API. Deno likes to leave the hard work behind
the scenes with its APIs and allows the user to implement their code as they like.
If you need the – watch flag, then the implementation of this code is left to you.
At the moment, the most popular development tool is Visual Studio Code with
the justjavac extension.vscode-deno.
Testing
The test tool is built into the Deno kernel using the Deno.test () function.
Validation functions such as assert, assertEquals, etc. are included in the
standard library. Therefore, the test code should look like this:
deno test
Integration Testing
Starting the development of your framework on Deno
(https://github.com/alosaur/alosaur), and by increasing the functionality on it, the
need for integration tests has increased dramatically. Fortunately, Deno already
had the best practices of such tests. To do this, use the function to start child
processes – Deno. run(). There is a special feature of such tests: you need to
release resources after passing them.
Compiler API
Deno supports runtime access to the built-in TS compiler. There are three
methods in the Deno namespace that provide this access: Deno.compile ()
Deno.bundle () Deno.transpileOnly()
compile () – this is if you called tsc via Node.js,
bundle () is the same as compile, except that instead of returning files, it
returns a single string, which is an ES module that includes all the code that was
included at the time of compilation. This command is also available from the
Deno bundle CLI.
For example, if the main module looked something like this:
It can be imported:
<script type="module">
import * as website from "website.bundle.js";
</script>
dev_server
It was with the help of transpileOnly that dev_server was implemented – a utility
for running your frontend code on Deno.
CI and CD
Of course, for effective development, it would be impossible to do without well-
built application delivery. For example, Deno itself was transferred to GitHub
actions.
To start working with TS, install the necessary tools. There are two ways to
install TS: via the npm package manager or as a plugin for Visual Studio.
When installing on macOS, you also need to enter the sudo command. When you
enter this command, the terminal will ask for the user's username and password
to install the package:
It is possible that TS was already installed earlier. In this case, you can update it
to the latest version using the command:
Compiling an App
First, create the application directory. In my case, it will be a folder along the
path C:/typescript. Add the file to the directory index.html. Open this file in any
text editor and define the following code1 in it:
1. The codes in this chapter are derived from
https://www.npmjs.com/package/typescript.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TypeScript HTML App</title>
</head>
<body>
<h1>TypeScript HTML App</h1>
<div id="content"></div>
<script src="app.js"></script>
</body>
</html>
This is a normal html file where the file is attached app.js. Now, in the same
directory, create the app.ts file. And it is app.ts, not app.js, that is, the TS code
file. It is also a plain text file. And in it we define the following content:
ar el = this.document.getElementById("content");
class user{
name : string;
age : number;
constructor (_name:string, _age: number){
this.name = _name;
this. age = _age;
}
}
var tom : User = new user ("Jack", 29);
el. innerHTML = "Name:" + jack.name + " age:" + jCK.age;
Here, we get the element with id=content, and create the User class. Next, we'll
look at creating and using classes. Creating an object of this class with the name
Jack and the age of 29 years. And output the object data to the element. When
saving a file, it is better to choose utf-8 encoding.
Now let's compile this file. To do this, in the command line/terminal, use the
cd command to go to the directory where the app.ts file is located (in my case,
this is C:\typescript). And to compile, run the following command:
tsc app.ts
After compilation, an app file is created in the project directory.js, which will
look like this:
var el = this.document.getElementById("content");
var User = (function () {
function User(_name, _age) {
this.name = _name;
this.age = _age;
}
return User;
}());
var jack = new User("Jack", 29);
el.innerHTML = "Name: " + jack.name + " age: " + jack.age
MERGING DECLARATIONS
TS uses several unique principles to describe JS objects at the type level. One
example of such a completely exclusive principle for TS is “ad merge.”
Understanding how this mechanism works gives you an advantage when
working with existing JS code, and also opens the door to more complex
abstraction principles.
In this chapter, “merging declarations” means that the compiler combines two
separate declarations with the same name into a single definition. The resulting
definition has properties that are common to both source declarations. Not only
two ads can be combined, but any number of ads can be combined.
Basic Concepts
In TS, a declaration creates entities in at least one of three groups: namespaces,
types, or values. Namespace-creating declarations create namespaces that
contain names that are accessible through the dot syntax. The type-creating
declarations create a type with the described form that is bound to the specified
name. Finally, value-generating declarations create values that are available in
the generated JS code.
Merging Interfaces
The simplest and perhaps most commonly used type of merge is interface merge.
At the simplest level, such a merge mechanically merges the members of both
declarations into a single interface with the same name.
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
Interface members that are not functions must be unique. The compiler will
throw an error if both interfaces define a member with the same name that is not
a function.
Each member function with the same name is treated as an overload
description for the same function. It is also worth noting that when merging
interface A with a subsequent interface A, the second one will have a higher
priority than the first one.
So, in this example:
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
Three interfaces will be merged together and you will get the following ad:
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
Note that the elements within the groups retain their order, but the groups
themselves are ordered so that the later overloads are at the beginning.
The only exception around this rule is specialized signatures. If the signature
has a parameter with the type of a single string literal (i.e., not a union of string
literals, for example), then it will rise to the top of the combined list of
overloads.
Merging Namespaces
Like interfaces, members of namespaces with the same names are also
combined. Since declaring a namespace creates both a namespace and a value,
you need to understand how they are all combined.
To merge namespaces, the type declarations from the exported interfaces in
each of the namespaces are combined to form a single namespace with the
combined interface definitions inside.
When merging namespace values, each definition is taken, and if a namespace
with that name already exists, it is expanded by adding exported members from
the second namespace.
In this example, the combined declaration is Animals:
namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
equivalent to:
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { }
export class Dog { }
}
This namespace merge model is not bad to start with, but you need to understand
what happens to members that are not exported. Non-exported members are only
visible in the original (non-merged) namespace. This means that after the merge,
they will not be visible to members from other ads. This can be seen more
clearly in the following example:
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles() {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // <-- error, haveMuscles isn't seen here
}
}
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
The visibility rules for the merged members are the same as those described in
the “Merging Namespaces” section, so AlbumClass must be exported to be
visible in the merged class. The final result is a class used from inside another
class. You can also use namespaces to add static members to existing classes.
In addition to the nested class technique, you are probably familiar with the
practice from JS of creating a function that is then extended by adding properties
to it. In order to create such structures type-safe, TS uses ad merge:
Prohibited Merges
Not all merges are allowed. Currently, classes cannot be combined with other
classes or with variables.
In addition to the traditional OO hierarchy, there is a way to create classes
from reusable components by combining simpler incomplete classes. The idea of
mix-ins or traits is also used in languages such as Scala. This approach has also
gained some traction in the JS user community.
The following code demonstrates the use of impurity in TS. The example will
be followed by a detailed explanation.
// Disposable mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
// Activatable mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
interact() {
this.activate();
}
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
////////////////////////////////////////
// Somewhere in your dynamic library
////////////////////////////////////////
The code2 begins with the definition of two classes that will be used as
impurities. Each of them is aimed at demonstrating a certain activity or
opportunity. Later, we will mix them to form a new class that combines their
properties.
2. The codes in this chapter are derived from
https://www.typescriptlang.org/docs/handbook/mixins.html.
// Disposable mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
// Activatable mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
Next, we will create a new class that will combine both admixtures. Let's look at
how to achieve this:
The first thing you might have noticed is that implements is used instead of
extends. This approach allows you to treat classes as interfaces and use only the
types Disposable and Activatable, and not their implementations. It turns out that
we will have to create the implementation in a new class. But the problem is that
this is exactly what we would like to avoid when using impurities.
To avoid doing the implementation again, we create stand-in properties,
whose types will be derived from the corresponding impurities. It is sufficient
for the compiler to have these elements available dynamically. This approach
allows us to take advantage of the impurities, but with the additional burden of
taking into account such nuances.
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
Let's write an auxiliary function designed to create impurities. It will run through
the properties of the impurities and copy them to the target element, filling the
duplicate properties with their implementations.
TypeScript Architecture
DOI: 10.1201/9781003203728-5
It is cheaper to maintain the code (hence, less time and financial costs).
Simplify the testability of the code (hence, you will need fewer testers
and lower losses due to missed “bugs on the prod”).
Accelerate the introduction of new developers into the project.
SINGLE-PAGE APPLICATION
Performance: Since SPA does not update the entire page but only the
necessary part, this significantly increases the speed of work.
High speed of development: Readymade libraries and frameworks
provide powerful tools for developing web applications. Backend and
frontend developers can work on the project in parallel. Thanks to a
clear separation, they will not interfere with each other.
Mobile apps: SPA makes it easy to develop a mobile application based
on readymade code.
MULTI-PAGE ARCHITECTURE
Each architecture has its own advantages and disadvantages and is well suited
for a particular type of project. SPA is distinguished by its speed and the ability
to develop a mobile application based on readymade code. But at the same time,
SPA has poor SEO optimization. Thus, this architecture is an excellent approach
for SaaS platforms, social networks, closed communities, where search engine
optimization does not matter.
MPA is more suitable for creating large online stores, business sites, catalogs,
marketplaces, etc. A well-optimized MPA has high speed and performance, but
still does not allow you to easily develop a mobile application. MPA and SPA
with the right architecture are well suited for developing scalable web
applications.
As an example, we will write a simple function that will output messages to the
console.
What a function should be able to do:
Output a message
Attach to the message any data that needs to be logged in
[Optional] Add a prefix to the message to understand which module
outputs the log
[Optional] Color the message depending on the type: log (normal
message), info (notification), warn (warning), error (error)
_log('Here is our user id:', 123); _log('Here is our user id:', 123, 'User Profile',
'info');
MAKING AN APP
console[type](message, args);
}
// Example of a call
_log('Test logging message', 123, 'Logger', 'info');
Compile .ts in .js. To do this, run the command in the project directory:
tsc logger.ts
If no errors occurred during compilation, the file should appear logger.js. At the
same time, if somewhere in our application there is a log with an incorrect type,
we will see a compilation error. Let's replace the example call with an erroneous
one:
That is, by strongly typing the arguments, we protected ourselves from errors
when calling our method. If you type all the methods of the application in this
way, its stability will increase. In addition, the compiler will not allow you to
build a script with syntax errors.
To avoid running the compilation manually each time, you can enable automatic
recompilation when files are changed:
MV* ARCHITECTURE
Many tasks that would normally be performed on the server side are performed
on the client side in SPAs. This has led to an increase in the size of JavaScript
applications and the need for more effective code organization.
As a result, developers have started using some of the design patterns that
have been successfully used in the back end over the past decade in the interface.
These include the Model-View-Controller (MVC) design pattern and some of its
derived versions, such as Model-View-ViewModel (MVVM) and Model-View-
Presenter (MVP).
Developers around the world have started sharing some SPA frameworks that
somehow try to implement the MVC design pattern, but don't necessarily
Strongly follow the MVC pattern. Most of these frameworks implement Models
and Views, but since not all of them implement controllers, we call this family of
frameworks MV*.
Model
The model in MVC provides the data. As a rule, this is a very simple POJO
(Plain Old JavaScript Object – good old Java object), which has certain
properties. As an example of a model, consider the following TypeScript class:
interface IModel {
DisplayName: string;
Id: number;
}
class Model implements IModel {
DisplayName: string;
Id: number;
constructor(model : IModel) {
this.DisplayName = model.DisplayName;
this.Id = model.Id;
}
}
let firstModel = new Model({Id: 1, DisplayName: ’firstModel’});
Here, we have defined an interface called IModel, which has the properties Id
and DisplayName, and a class that implements this interface. We have provided
a simple constructor to set these properties. The last line of this fragment creates
an instance of this class with the desired properties. As you can see from this
snippet, the Model class is a very simple POJO that contains some data.
View
The view in MVC provides a visual representation of the model. In web
frameworks, this will usually be a snippet of HTML code:
<div id="viewTemplate">
<span> {Id} </span>
<span><h1> {DisplayName} </h1></span>
</div>
There is a div tag here that contains two span tags. The contents of the first span
tag are highlighted in bold and will display the Id property from the model. The
content of the second span tag is the h1 header and displays the DisplayName
property from the model. By separating the elements of the user interface view
from the model, we can see that we can change the view as we like, even without
using the model code. We can apply styles to each element using CSS, or even
completely hide certain properties in the view. This separation gives us the
ability to design or modify a part of the display regardless of the model.
This design work can even be transferred to a completely separate and
independent team with specialized skills in the field of user interface design. As
long as the underlying model doesn't change, both parts of the model and view
will work together seamlessly.
Let's look at the following example:
class View {
template: string;
constructor(_template: string) {
this.template = _template;
}
render(model: Model) {
// Combining the template and the view;
}
}
Here, we have defined a View class that has a single template property. When
we create this view, we give it an HTML template that it should use. This View
class also has a render method with a single model argument. The render method
will combine the template and the model and return the final HTML code.
Controller
The controller in the MVC framework does the job of coordinating the
interaction between the model and the view. The controller usually performs the
following steps:
The controller in MVC is also responsible for the application logic. This means
that it can control which views are presented, when and what to do when certain
events occur.
As an example of what a controller might look like, let's look at the following
code:
class Controller {
model: Model;
view : View;
constructor() {
this.model = new Model({Id : 1, DisplayName : 'firstModel'});
this.view = new View($('#viewTemplate').html());
}
render() {
$('#domElement').html(this.view.render(this.model));
}
}
Here we have defined the Controller class, which has the model and view
properties. Then our constructor function creates an instance of the Model class
with certain properties and an instance of the View class. An instance of the
View class is created using a template that is read from the viewTemplate DOM
element. The Controller class also defines the render function, which sets the
actual HTML code of the DOM element DOMElement. This HTML code is the
result of calling the render function for the View class and passing the model for
rendering.1
1. The image is taken from https://habr.com/en/post/151219/.
1. When a user visits a web resource, the initialization script creates an instance
of the application and runs it for execution.
2. The index action of the front controller is executed, which generates a view
of the main page.
3. The view is displayed to the user.
The first three steps are a simple chain, without using a model. Next is the
sequence where the model is involved:
4. After the application receives a request from the user, an instance of the
requested controller is created and the specified action is called.
5. This action calls the methods of the model that modify it.
6. The view is generated (or the view is notified when the model is updated).
7. The view requests data to be displayed.
8. The model returns the requested data.
9. The view displays the results to the user.
MVC FRAMEWORKS
BACKBONE
tsc-init
Initialize npm and install Backbone, Bootstrap, JBone, and the declaration files
for Backbone using the @types:
AURELIA
Aurelia was one of the first SPA frameworks to offer full TypeScript integration.
It is a framework that uses the capabilities of ECMAScript 6 to improve the
development experience. One of the most striking features of Aurelia is the small
amount of code you need to write to get things done.
Aurelia understands that if you are writing a standard class, you will probably
want to use the class properties to render the HTML code. Of all the frameworks
that we will discuss, Aurelia is the easiest to use and the most intuitive. There are
no hidden bugs or special workarounds. This framework has gone to great
lengths to simplify the TypeScript development process.
The easiest way to set up a development environment is to use the Aurelia
command-line interface aurelia-cli, which can be installed as follows:
au new
You will be asked a simple set of questions, starting with the name of the base
catalog you would like to use. The next question is whether to use ESNext or
TypeScript as the development language, and the last question is whether or not
to load all the project dependencies. Select TypeScript and then Yes to load the
dependencies. It will take a few minutes to set up the default project structure.
After that, a new directory will be created based on the project name you
selected at the beginning of the process.
The aurelia-cli program has several options. To compile your project, enter:
au build
au run
This is followed by the compilation and binding steps, and then configuring the
http server to serve the default application on port 9000.
ANGULAR
ng new my-app
The Angular command line interface is designated as ng, and here we have
specified that it should create a new project in a new directory named my-app. In
the new directory, the command-line interface will download and install all the
necessary components of the Angular application, as well as create a minimal
project instance in the src/app directory to get you started. To start the
development web server and see how this application works, type the following
command:
npm start
The start command will compile all the source code of the application and
launch the web server on port 4200.
Along with compiling the application and automatically starting the web
server, the npm start command will also look at the source files in the project
directory and automatically recompile the application after changing the files. It
will also give the web browser a signal to restart the app. The built-in
monitoring, recompilation, and reloading capabilities are very helpful in web
application development, providing quick feedback when the source code
changes. Keep an eye on the console where you are running npm start. It shows
all TypeScript compilation errors that occur when saving your files.
REACT
Despite the fact that React is more of a library than a framework, it stands
behind the user interface of Facebook and Instagram, showing its effectiveness
inside dynamic applications with high traffic (bandwidth).
It is rightfully considered the fastest growing JS framework: today there are
about 1000 GitHub authors. In the MVC (Model-View-Controller) React
model.js acts as a “V” and can be easily integrated into any architecture. Thanks
to the use of a virtual DOM tree, it provides a greater performance boost
compared to Angular 1.x. In addition, React components can be created and
reused in other applications, or even transferred for public use.
Although React is more difficult to learn, it makes application development
simple and easy to understand. In addition, it can be ideally suited for complex,
impressive software solutions with a high degree of load.
It uses a specific built-in syntax to combine HTML templates and JavaScript
code into a single file called JSX. It doesn't have downloadable string templates,
like in Backbone, or HTML code snippets that are in a separate file, like in
Angular or Aurelia. In React, all templates are mixed with regular JavaScript
code, using an HTML-like syntax. As a simple example of this syntax, let's look
at the following code:
render() {
return <div>Hello <span>React</span></div>;
}
Here, we have the standard TypeScript render function. As part of this function,
we return what looks like native HTML code with <div>tags and child
<span>tags. Note that there are no quotation marks around these HTML
elements. They are written inside our function without a clear separation from
the regular TypeScript code.
TypeScript included support for the unique React/JSX syntax in release 1.6.
However, to use the new JSX syntax, we will need to create TypeScript files
with the extension .tsx instead of the usual extension. ts. When TypeScript finds
files with the extension. tsx, it parses the file as a JSX file, which allows you to
use the JSX syntax.
The process that React uses to generate JavaScript from JSX files is an
additional step in the normal development workflow. Our TypeScript files .tsx,
after compilation, will generate JavaScript files that convert the JSX syntax into
a series of calls to React libraries.
For example, using the <div> element in .tsx file will create a call to
React.createElement (“div”,…) in the compiled JavaScript file. These compiled
files must then be combined with the React libraries themselves to create
executable code. For this reason, it is recommended to use a tool such as
Webpack to combine the output of the compilation step with the React libraries.
Webpack will also create a single output file to load into the browser in a process
called bundling.
To start a new React project, we will follow a few steps. First, create a
directory for your project and initialize npm:
mkdir react-sample
cd react-sample
npm init
Here, we create a directory for our project, go to it, and initialize npm in the
project directory. After that, the package file will be created.json, which can use
npm. After initialization, we can install webpack:
The webpack will be installed as a global Node module and the webpack
command-line interface, webpack-cli, will be installed. Note that, although we
have webpack installed globally, the webpack command-line tool should still
find webpack modules in the node_modules directory. This means that we also
need to install webpack as a local module.
After that, the react and react-dom libraries will be installed in the node_modules
directory. We will need a number of other utilities, namely:
After installing Bootstrap, we can install react ad files using the @types syntax:
Webpack Configuration
As indicated earlier, webpack is used to combine TypeScript output with React
libraries and create a single unified JavaScript file that can be used in the
browser. To do this, however, use the compilation options for TypeScript in the
tsconfig file.json and webpack configuration in the webpack.config file.the js
must be consistent. The easiest way to do this correctly is to use one of the
convenient configuration tools available online. One of these tools can be found
on the page https://webpack.jakoblind.no. It provides a simple HTML page for
configuring a number of options available for webpack.
In this chapter, we took a detailed look at what an MVC framework is, and
discussed each of its elements. We have covered the roles and responsibilities of
the model, view, and controller in MVC, and how they interact with each other
when creating user interfaces. We also briefly discussed the benefits of using
MVC frameworks. We then explored four MVC frameworks that are either very
tightly integrated with TypeScript or written with TypeScript in mind. We also
discussed the factors that affect performance when working with each of these
frameworks.
TEST-DRIVEN DEVELOPMENT
Automated testing can be divided into three main areas, or types of testing:
module testing, integration testing, and acceptance testing. You can also describe
these testing as testing from the “black box” or from the “white box.” White-box
testings are those in which the internal logic or structure of the code under
testing is known to the tester. Black-box testings, on the other hand, are those in
which the internal design and/or logic is unknown to the tester.
Module Testings
A module testing is usually a white-box testing in which all the external
interfaces of a code block are mocked or muted. If we are testing some code that,
for example, performs an asynchronous call to load a JSON block, module
testing of this code will require locking the returned JSON. This method ensures
that the object under testing is always given a known set of data. When new
requirements arise, this well-known data set can grow and expand, of course.
The objects under testing should be designed to interact with interfaces, so that
these interfaces can be easily mocked or silenced in a module testing scenario.
Integration Testings
Integration testings are another form of white-box testings that allow the object
under testing to run in an environment close to real-world code. In our previous
example, when some code executes an asynchronous call to load a JSON block,
the integration testing actually needs to call the REST services that generate the
JSON. If this REST service relies on data from the database, then the integration
testing will require data in the database that matches the integration testing
scenario. If we were to describe a module testing as having a boundary around
the object under testing, then an integration testing is simply an extension of that
boundary to include dependent objects or services.
Creating automated integration testings for your applications will significantly
improve the quality of your product. Consider the case of the scenario we used
when a block of code calls the REST service for data in JSON format. Someone
can easily change the structure of this data returned by the REST service. Our
unit testings will still pass, as they don't actually call the REST code on the
server side, but our application will be messed up because the returned JSON
doesn't meet our expectations.
Without integration testings, these types of errors will only be detected at later
stages of manual testing. By thinking about integration testings, by
implementing specific data sets for integration testings and embedding them in
your own set, you can quickly eliminate such errors.
Acceptance Testings
Acceptance testings are black-box testings and are generally scenario-based.
They can include multiple custom screens or custom interactions to pass
through. These tests are also commonly performed by the test team, as they may
require logging in to the application, searching for a specific dataset, updating
data, and so on. Using planning and the many tools available, we can also
automate these acceptance tests so that they run as part of an automated test
suite. The more acceptance tests a project has, the more reliable it will be. Note
that in the test-driven development methodology, each error detected by the
manual testing team should lead to the creation of new unit, integration, or
acceptance tests. This methodology will help ensure that once an error is found
and fixed, it will not appear again.
REFACTORING
Refactoring is a systematic procedure for improving code without creating new
functionality. Refactoring turns mess into clean code and simple design.
As trivial as it may sound, refactoring in a large project is a complicated thing,
especially if the tests in the project are not written for every function or are
written in such a way that they do not cover the entire code base or not all use
cases. Even by slightly changing the behavior of a function, you always run the
risk of returning something from it or passing (forgetting to pass) something
wrong to it. This case is more in the piggy bank of tests, but still with TypeScript
you can be sure that the function returns the type that you specified and accepts
exactly what you specified.
Microsoft has released TypeScript 4.2. This is the most recent update at the time
of writing this book. In the quarterly update of the language, it is now possible to
set the abstract modifier to the constructor signature and mark the variable as
unused when the array is destructured. Tuple types now allow rest arguments at
any position, and type aliases are no longer expanded when a hint is displayed.
The tuple syntax in TypeScript supports rest elements that are prefixed with […]
and can contain any number of parameters of the same type. Previously, rest
parameters were put in the last position, so it was impossible to express tuple
types that end with a fixed set of elements. Such tuple types are useful for
strongly typed functions with lists of variable parameters that end with a fixed
set of parameters.
Tuple types now allow rest parameters at any position, not just the last one.
For example:
Now there is only one restriction for rest elements – they cannot go after other
rest elements and before optional elements.
SAVING-TYPE ALIASES
Type aliases are now saved for more correct display of the hint in the IDE and
type output in the .d.ts definition files. It also tracks type aliases for instances of
other aliases. This makes the representation of composite types readable
(including in the definition file. d.ts).
TypeScript allows you to mark a class as abstract – in this case, it is only used to
extend it with a subclass, and object instances are created only for a specific
implementation class. But in TypeScript of previous versions, there were
problems when creating mix-ins that use abstract classes.
TypeScript 4.2 allows you to specify an abstract modifier for constructor
signatures. Adding an abstract modifier to the constructor signature signals that
an abstract class can be passed to it. This does not prevent you from passing
other classes and constructor functions that are “specific” to it.
Mix-in factories will now support abstract classes.
"foo" in 42
/ / Error! The right side of the in expression must not be a
primitive.
Now, when destructuring an array, you can explicitly mark variables as unused
by adding an underscore to them.
Previously, when the noUnusedLocals option was enabled, this syntax gave an
error: “Variable declared, but not read.” Now, for array variables that have an
underscore prefix, the error will not be returned.
TYPE ARGUMENTS
Type arguments are not allowed in JavaScript, but in TypeScript 4.2, the parser
will parse them in a specification-compliant way. When writing the following
code in a JavaScript file:
This is important if you have used the TypeScript API to analyze type constructs
in JavaScript files.
In addition, there is a new noPropertyAccessFromIndexSignature option. This
function disables the ability to use a point to access those object properties that
are defined using the string index signature.
If there is a function inside the Boolean expressions with && and || without
calling it, this will result in a compilation error under the – strictNullChecks flag.
In the newest version of TypeScript, they also added support for the -
explainFiles flag. You can use it to understand why the file was included in the
compilation process.
Future Plans
The next quarterly version of TypeScript 4.3 was released on May 25. The
iteration plan lists upcoming language features, editor performance
improvements, performance improvements, infrastructure improvements, and
more.
In addition, according to the TypeScript roadmap, Microsoft plans to:
Remember that in 2020, Microsoft revealed the source code of the TypeScript
library for editing documents in the cloud.
CHAPTER 6
Appraisal
DOI: 10.1201/9781003203728-6
TypeScript is both a language and a set of tools for generating JavaScript code. It
was developed by Anders Heilsberg at Microsoft Corporation to help developers
write enterprise-wide JavaScript code. This book begins with an introduction to
the TypeScript language, and gradually moves from basic concepts to advanced
and powerful features of the language, including asynchronous programming
methods, decorators, and generalizations. Many modern JavaScript and
TypeScript frameworks are also considered in parallel. Some of the well-known
object-oriented methods and design patterns are described, and their real-world
implementations are presented. Using this book, you can create a
comprehensive, end-to-end web application that shows you how to combine
TypeScript features, design patterns, and best development practices in a real-
world scenario.
TypeScript is a very powerful tool that allows you to significantly improve the
readability and security of your code. In this book, we have looked at TypeScript
both inside and out. In the first chapter of this book, we compared JavaScript
with TypeScript and noted the strengths of TypeScript. TypeScript is statistically
typed and this makes it easier to use. In addition to this, statistical typing has
such a feature as auto-completion in code editors, which makes the process of
writing code easier, because you do not need to constantly look through the
documentation and search for the name of the method you need. After that, we
moved on to the part where we analyzed how ECMAScript works with
TypeScript. TypeScript is a compiled superset of JavaScript, bringing optional
static typing and some of the features of modern ECMAScript standards. The
TypeScript compiler has a parameter that can switch between different versions
of the ECMAScript standard and currently TypeScript supports ES3, ES5, and
ES6.
Also, in parallel, a lot of modern JavaScript and TypeScript frameworks are
considered – for each of them, module and integration testing is described in
detail. Some of the well-known object-oriented methods and design patterns are
described, and their real-world implementations are presented.
TypeScript brings many benefits to developer productivity and developer
experience. TypeScript is not unique to Angular, other powerful frontend
frameworks such as React are beginning to be used with TypeScript to allow
development teams to build robust, resilient, and scalable applications.
JavaScript and TypeScript are constantly evolving but do not compete with each
other. TypeScript was created to complement and improve JavaScript, not to
replace it. In the future, they may become very similar in their functions, but
TypeScript will remain a statically typed alternative.
If you're new to JavaScript, you shouldn't grin at the first mention of
TypeScript. TypeScript was developed not just to encourage backend developers
to work with the frontend, but to build on lessons learned from other languages
to provide everyone with a tool that allows you to create reliable code.
JavaScript was created to be portable and simple – but over time, we have
seen that the language goes beyond its original intent. Although a few hundred,
if not thousands of lines of code, could still be supported in regular JavaScript –
we all know that this is no longer the case. JavaScript code works everywhere
and on almost every device. The number of rows is growing exponentially, and
we need a way to mentally digest and maintain it.
It is not difficult to learn TypeScript itself. It is written and runs like
JavaScript – with only a few tools and techniques to help you improve the code.
In fact, you can just write TypeScript code like JavaScript, and it will still
compile. While TypeScript may seem like syntactic sugar, it makes our
experience of maintaining and writing code much more optimal.
I hope that by the end of this book, you will be able to create a
comprehensive, end-to-end web application that shows you how to combine
TypeScript features, design patterns, and best practices in a real-world scenario.
Appendix
Types
Usage
Scope/Modifiers
Public (default) public firstName: string;
Protected protected inventory: number;
Private private outOfStock: boolean;
Read Only readonly pi: number = 3.14159;
Static static log(msg: string) { console.log(msg) };
Classes
class OrderLogic {
constructor(public order: IOrder) { }
getOrderTotal(): number {
let sum: number = 0;
Abstract Classes
abstract class Person {
name: string;
monthlySalary: number;
monthlyBenefits: number;
Interfaces
interface IOrderDetail {
productName: string;
quantity: number;
price: number;
orderDate: Date;
shipped: boolean;
//Optional
outOfStock?: boolean;
//Method
calcTax: (taxRate: number) => number;
}
Inheritance and Implementing Interfaces
interface IGPS {
getLocation() number;
}
class Vehicle {
make: string;
model: string;
year: number;
}
getLocation(): number {
}
}
Optional Parameters
class Util {
log(msg: string, logDate?: Date) {
if (logDate)
console.log(logDate + ’ ’ + msg);
else
console.log(new Date() + ’ ’ + msg);
}
}
Rest Parameters
class Order {
addOrderDetails(…orderDetails: IOrderDetail[]) {
}
}
Namespaces
namespace AcmeCorp.Logging {
export class Logger {
static log(msg: string): void {
console.log(msg);
};
}
}
//Alias
import logger = AcmeCorp.Logging.Logger;
namespace AcmeCorp.OnlineStore {
class OrderLogic {
calcOrder(): number {
logger.log("calculating order");
return 0;
}
}
}
Compiler Comments
A
Abstract classes, 60–61, 178
Abstract modifier in constructor signatures, 171–172
Acceptance testings, 169–170
Access modifiers, 51
accessors (getters/setters), 58–59
parameter properties, 58
private modifier, 52–53
protected modifier, 53–54
public by default, 51–52
readonly modifier, 55
static properties, 59–60
type iterator modifier, 55–57
Accessors (getters/setters), 58–59
addClickListener, 78, 79
addVar variable, 68
Ad merge, 142
AlbumClass, 145
AMD, 105
Angular, 162–164
Angular 2, 159, 162–163
Angular Command Line Interface, 163
AngularJS, 163
Animal class, 66
Anonymous functions, 68–69
any type, 15–16
App
compiling, 140–141
making, 154–155
Application architecture, 151
abstract modifier in constructor signatures, 171
in statement, strict checks for, 172
acceptance testings, 169–170
Angular, 162–164
Aurelia, 161–162
Backbone, 160–161
compilation, 155
creating an application in TS, 153–154
integration testings, 169
making an app, 154–155
Model-View-Controller (MVC) frameworks, 159–160
Model-View-Controller (MVC) pattern, 155
controller, 157–159
model, 156
view, 156–157
module testings, 168–169
multi-page application (MPA), 152–153
MV* architecture, 155
need for, 151–152
React, 164
webpack configuration, 166
refactoring, 170
rest elements, support for, 170–171
saving-type aliases, 171
single-page application (SPA), 152
test-driven development, 166–168
tuple syntax in TS, 170–171
type arguments, 172
future plans, 173
TypeScript 4.2 release, 170
unused variables during array destructurization, 172
Array<T> type, 37
Array destructurization, unused variables during, 172
Array type, 10
AtScript language, 159
Aurelia, 161–162
aurelia-cli program, 162
Automatic type inference, 84
B
Backbone, 160–161
BaseClass, 50
baseUrl, 121
Best general type, 66–67
Black-box testings, 168
Block scope viewability, closure of variables with, 25–26
Block viewport, 22–23
Boolean, 9
Boolean values, 11, 80, 97
Browser-based JS, 128
Bundle, 138
C
C#, 2
Callback functions, this parameters for, 78–79
cardPicker(), 76
Cheat sheet, TS, 177–180
abstract classes, 178
classes, 178
compiler comments, 180
inheritance and implementing interfaces, 178–179
interfaces, 178
namespaces, 179–180
optional parameters, 179
rest parameters, 179
scope/modifiers, 177
triple slash directives, 180
types, 177
usage, 177
Choosing TS, 2–4
Classes, 47, 178
abstract classes, 60–61
access modifiers, 51
accessors (getters/setters), 58–59
parameter properties, 58
private modifier, 52–53
protected modifier, 53–54
public by default, 51–52
readonly modifier, 55
static properties, 59–60
type iterator modifier, 55–57
constructors, 61
using a class as an interface, 63–64
inheritance, 48
class inheritance, 50–51
interface inheritance, 49–50
classicstrategy, 118
Class inheritance, 50–51
Class types in generalizations, 90
new keyword, 91
closedDoor variable, 14
Coding, 3
CommonJS, 105
Compiler, 19–20, 180
Compiling an app, 140–141
Constant declarations, 26–27
Constant enums, 93–95
Constructor function, 62
Constructors, 61
using a class as an interface, 63–64
Constructor signatures, abstract modifier in, 171
strict checks for the in statement, 172
Context type, 67–68
Contextual typing, 66
createCardPicker function, 76, 78
createSquare, 36
D
Declare enums, 95–96
Declaring functions, 29
Default values, 28–29
Delphi, 2
Deno, 128
compiler API, 137–139
debugging, 136–137
installation of, 130
Deno.core, 134
Deno modules, 133–134
Deno standard modules, 134
Deno X, 134
internal part, 131–132
lock file, 135
package manager, 134–135
preset config by default, 133
safety, 132–133
WASM, RUST, plugins, 136
web standards, 135
in Windows, 130–131
integration testing, 137
main features of, 129
module system, 129
safety, 129
testing, 137
DerivedFromBaseClass, 50
Destructuring, 27–28
dev_server, 138–139
Discriminated union types, 33
extra properties, checks for, 38–40
functional types, 40–41
indexed types, 41–43
interfaces, working with, 35
optional properties, 36–37
read-only properties, 37–38
DisplayName property, 157
doAnimalsHaveMuscles function, 145
DoorState enumeration, 13–14
E
ECMAScript, 175–176
ECMAScript 3 standard, 1
ECMAScript 5 standard, 5
ECMAScript 6 standard, 76
ECMAScript 2015 standard, 27, 96
Ellipsis, 73
Employee subclass method, 54
Enums, 13–15, 92
constant enums, 93–95
declare enums, 95–96
ESNext, 162
explainFiles flag, 173
Export, 101
default export, 103–105
reexport, 102–103
validation, 102
Export default, 112–113
Exporting multiple objects, 113
Extending interfaces, 32
External modules, 110
abbreviated external module declaration entry, 111
module declarations using wildcard characters, 111
Extra properties, checks for, 38–40
F
firstName parameter, 71, 72
Functional types, 40–41
Functions, 18, 64
overloads, 80–82
type compatibility, 69
optional parameters and default parameters, 70–73
rest parameters, 73
“this” and arrow functions, 75–77
this keyword, 74–75
this parameters, 77–78
this parameters for callback functions, 78–79
types of
adding types to a function, 64–65
anonymous functions, 68–69
best general type, 66–67
context type, 67–68
inferring types, 65–66
G
Generalizations, limitations of, 88–90
Generalized classes, 88
Generalized types, 86–87
Generalized type variables, working with, 84–86
GenericIdentityFn, 87
GenericNumber type, 88
Generics, 83
exploring the world of, 83–84
generalizations, limitations of, 88
using generic parameters in generalization constraints, 89–90
generalized classes, 88
generalized types, 86–87
generalized type variables, working with, 84–86
using class types in generalizations, 90
new keyword, 91
GitHub, 130, 139
Go language, 134
Google, 159
Greeter class, 48, 62
greeterMaker, 63
H
Handler class, 79
Hejlsberg, Anders, 1, 175
History of TS, 1–2
Horse class, 49
I
IBase, 50
IDE, see Integrated development environment
IDerivedFromBase interface, 50, 51
IFirstInterface interface, 51
Import, 103
default export, 103–105
importing a module for the sake of “side effects”, 103
importing a single exported item, 103
importing the entire module into a single variable, 103
Imported names, explicitly defining, 113
Indexed types, 32, 41–43
Inferring types, 65–66
Inheritance, 48
class, 50–51
and implementing interfaces, 178–179
interface, 49–50
Installation of TS
via NPM, 139–140
as a visual studio plugin, 140
In statement, strict checks for, 172
Integrated development environment (IDE), 4, 43
node-based compilation, 43
tsconfig.json file, creating, 44–45
Integration testings, 169
Interface implementation, 32–33
Interface inheritance, 49–50
InterfaceInheritanceClass class, 50
Interfaces, 30, 178
extending interfaces, 32
indexed types, 32
interface implementation, 32–33
merging, 142–144
working with, 35
ISecondInterface, 51
isEligible function, 4
Isomorphic modules, 112
J
JavaScript (JS), 1, 5, 80, 82, 110, 166, 168, 175–176
and TypeScript (TS), 3–4
JS, see JavaScript
JSDoc, 5
K
keepWholeObject function, 29
L
LabelledValue interface, 35
Language service, 8
lastName parameter, 71, 72
.length function, 85
Let keyword, 19
Loading modules, 115–117
loggingIdentity type, 85
M
Merging declarations, 142
basic concepts, 142
interfaces, merging, 142–144
namespaces, merging, 144–145
with classes, 145–146
prohibited merges, 146–149
Microsoft Corporation, 175
Model-View-Controller (MVC) frameworks, 155, 159–160, 162
controller, 157–159
model, 156
view, 156–157
Model-View-Presenter (MVP), 155
Model-View-View-Model (MVVM) approaches, 155, 162
Module, 100
export, 101
export validation, 102
reexport, 102–103
external modules, 110
abbreviated external module declaration entry, 111
module declarations using wildcard characters, 111
generating code for, 105–108
import, 103
default export, 103–105
importing a module for the sake of “side effects”, 103
importing a single exported item, 103
importing the entire module into a single variable, 103
optional module loading, 108–110
working with other JavaScript libraries, 110
Module declaration
abbreviated external module declaration entry, 111
using wildcard characters, 111
Module resolution, tracking, 124–125
Module resolution strategies, 118
classic, 118–119
node, 119
Module resolution system, additional flags of, 120–121
Modules, structuring, 112
explicitly defining the imported names, 113
exporting as close to the top level as possible, 112
exporting multiple objects, 113
exporting only one class or one function, 112–113
importing a large number of items, 113–114
loading modules, 115–117
module resolution, 117
namespaces in modules, 114
path mapping, 121
tracking module resolution, 124–125
virtual directories with rootDirs, 123–124
trace, 126
relative and nonrelative module imports, 117–118
additional flags of the module resolution system, 120–121
base URL, 121
module resolution strategies, 118–119
Node.js, 119–120
risk indicators, 114
Module system, 129
Module testings, 168–169
MouseEvent parameter, 67
MPA, see Multi-page application
Multi-page application (MPA), 152–153
MultipleInterfaces, 51
Multiple objects, exporting, 113
MV* architecture, 155
MVC frameworks, see Model-View-Controller frameworks
MVP, see Model-View-Presenter
MVVM approaches, see Model-View-View-Model approaches
N
Namespace, 99–100, 179–180
import template, 113–114
merging, 144–145
with classes, 145–146
in modules, 114
Netscape, 5
Never type, 17–18
Node.js, 119–120, 128
Node-based compilation, 43
tsconfig.json file, creating, 44–45
Node variant, 43
noPropertyAccessFromIndexSignature option, 172
noUnusedLocals option, 172
NPM, installation of TS via, 139–140
Null and Undefined types, 17
Number, 9
O
Octopus class, 58
onClickBad function, 79
onClickGood function, 79
openDoor variable, 14
OpenSource, 2
Optional module loading and its other advanced scenarios, 108–110
Optional parameters, 179
Optional properties, 36–37
Optional tuple elements, 12
optionTuple property, 12
P
Parameter properties, 58
Path mapping, 121
module resolution, tracking, 124–125
rootDirs, virtual directories with, 123–124
trace, 126
Person class, 54
pickCard function, 81, 82
Predefined characters, 97–98
Private modifier, 52–53
Prohibited merges, 146–149
Protected modifier, 53–54
R
React, 164
webpack configuration, 166
ReadonlyArray, 38
ReadonlyArray<T> type, 37
Readonly modifier, 55
Read-only properties, 37–38
Re-declaring and escaping, 23–25
Reexport, 102–103
Refactoring, 170
Relative and nonrelative module imports, 117–118
base URL, 121
module resolution strategies, 118
classic, 118–119
node, 119
module resolution system, additional flags of, 120–121
Node.js resolving modules, 119–120
Renaming properties, 28
Rest elements, support for, 170–171
Rest parameters, 73, 179
REST service, 169
Risk indicators, 114
rootDirs, virtual directories with, 123–124
Runtime, 127
compiling an app, 140–141
Deno, 128
compiler API, 137–139
debugging, 136–137
installation of, 130–136
integration testing, 137
main features of, 129
testing, 137
installation of TS via NPM, 139–140
installing TS as a visual studio plugin, 140
merging declarations, 142
basic concepts, 142
interfaces, merging, 142–144
namespaces, merging, 144–146
prohibited merges, 146–149
Node.js, 127–128
static typing, 128
Runtime error, 172
Rust backend, 132
Rusty V8, 131–132
S
Saving-type aliases, 171
Scoping, 21–22
SearchFunc interface, 41
Server-side JS, 128
Single-page application (SPA), 152
someFunc function, 95
SPA, see Single-page application
SquareConfig definition, 40
SquareConfig objects, 39
squareOptions, 39
Static properties, 59–60
Static typing, 128
Strengths, of TS, 5–6
strict-NullChecks flag, 173
String, 9–10
stringArray interface, 42
String enums, 15
Structure of TS, 7
basic TS compiler, 8
constant declarations, 26–27
declaring functions, 29
default values, 28–29
destructuring, 27–28
discriminated union types, 33
checks for extra properties, 38–40
functional types, 40–41
indexed types, 41–43
optional properties, 36–37
read-only properties, 37–38
working with interfaces, 35
interfaces, 30
extending interfaces, 32
indexed types, 32
interface implementation, 32–33
language service, 8
object-oriented programming, 7
renaming properties, 28
tool integration, 8
tsc, 8
TS compiler, 19–20
type assertions, 18
let keyword, 19
type check, providing, 7
types of TS, 8
any type, 15–16
array type, 10
Boolean, 9
enum, 13–15
never type, 17–18
Null and Undefined types, 17
number, 9
optional tuple elements, 12
string, 9–10
string enums, 15
Symbol type, 18
tuple, 10–11
tuples deconstruction, 11–12
void, 16
variable declarations, 20–21
block scope viewability, closure of variables with, 25–26
block viewport, 22–23
re-declaring and escaping, 23–25
scoping, 21–22
sumMatrix function, 22, 24
Symbols, 96
predefined characters, 97–98
Symbol type, 18
Syntactic sugar, 6–7
SystemJS, 111
T
Test-driven development, 166–168
“this” and arrow functions, 75–77
this keyword, 74–75
this parameters, 77–78
for callback functions, 78–79
Tokio, 132
Tool integration, 8
Triple slash directives, 180
tsc, 8
tsconfig.json file, creating, 44–45
TS layer, 131
Tuple, 10–11
Tuples deconstruction, 11–12
Tuple syntax in TS, 170–171
Type arguments, 172
future plans, 173
Type assertions, 18
let keyword, 19
Type check, providing, 7
Type compatibility, 69
optional parameters and default parameters, 70–73
rest parameters, 73
“this” and arrow functions, 75–77
this keyword, 74–75
this parameters, 77–78
for callback functions, 78–79
Type iterator modifier, 55–57
TypeScript 4.2, 170, 171
Types of TS, 8
any type, 15–16
array type, 10
Boolean, 9
enum, 13–15
never type, 17–18
Null and Undefined types, 17
number, 9
optional tuple elements, 12
string, 9–10
string enums, 15
Symbol type, 18
tuple, 10–11
tuples deconstruction, 11–12
void, 16
U
UMD modules, 111
Union type, 10–11
Unused variables during array destructurization, 172
V
V8 platforms, 131–132
Variable declarations, 20–21
block scope viewability, closure of variables with, 25–26
block viewport, 22–23
re-declaring and escaping, 23–25
scoping, 21–22
Virtual directories with rootDirs, 123–124
Visual Studio, 43
Visual Studio Code, 137
Visual studio plugin, installing TS as, 140
Void, 16
W
Watch mode, 155
White-box testings, 168
Wildcard characters, module declarations using, 111
Windows, installation of Deno in, 130–131
Windows variant, 43