b
First steps with TypeScript
After the brief introduction to the main principles of TypeScript, we are now ready to start our journey toward becoming FullStack TypeScript developers. Rather than giving you a thorough introduction to all aspects of TypeScript, we will focus in this part on the most common issues that arise when developing Express backends or React frontends with TypeScript. In addition to language features, we will also have a strong emphasis on tooling.
Setting things up
Luckily there is not much setup here as WebStorm works natively with TypeScript. Just go ahead and download this new empty repo by visiting this site: http://go.djosv.com/227labtsintro
As mentioned earlier, TypeScript code is not executable by itself. It has to be first compiled into executable JavaScript. When TypeScript is compiled into JavaScript, the code becomes subject to type erasure. This means that type annotations, interfaces, type aliases, and other type system constructs are removed and the result is pure ready-to-run JavaScript.
In a production environment, the need for compilation often means that you have to set up a build step. During the build step, all TypeScript code is compiled into JavaScript in a separate folder, and the production environment then runs the code from that folder. In a development environment, it is often easier to make use of real-time compilation and auto-reloading so one can see the resulting changes more quickly.
Let's start writing our first TypeScript app. To keep things simple, let's consider using the npm package ts-node. It compiles and executes the specified TypeScript file immediately so that there is no need for a separate compilation step.
You could install both ts-node
and the official typescript
package globally by running:
npm i -g ts-node typescript
However, if you can't or don't want to install global packages, you can create an npm project which has the required dependencies and run your scripts in it. Let's take this approach.
As we recall from part 3,
an npm project is set by running the command npm init
in an empty directory.
You can do this now from inside your repository.
Afterward, we can install the dependencies by running:
npm i -D ts-node typescript
and setting up scripts within the package.json:
{
// ..
"scripts": {
"ts-node": "ts-node" },
// ..
}
You can now use ts-node
within this directory by running npm run ts-node
.
Notice that if you are using ts-node
through package.json,
command-line arguments that include short or long-form options for the npm run script
need to be prefixed with --
.
So if you want to run file.ts with ts-node
and options -s
and --someoption
, the whole command is:
npm run ts-node file.ts -- -s --someoption
It is worth mentioning that TypeScript also provides an online playground, where you can quickly try out TypeScript code and instantly see the resulting JavaScript and possible compilation errors.
Pertinent: The playground might contain different tsconfig rules (which will be introduced later) than your local environment, which is why you might see different warnings there compared to your local environment. The playground's tsconfig is modifiable through the TS Config dropdown menu.
Configuration and coding style
Let's add a configuration file tsconfig.json to the project.
In WebStorm you can generate one via File->New->tsconfig.json file.
Then, add the noImplicitAny field to the compilerOptions
object
{
"compilerOptions":{
// ...
"sourceMap": true,
"noImplicitAny": false }
}
The tsconfig.json file is used to define:
- how the TypeScript compiler should interpret the code
- how strictly the compiler should work
- which files to watch or ignore,
- and much more
For now, we will add the compiler option noImplicitAny
,
which will not require that we specify types for all variables.
JavaScript can be written in a multitude of ways; it's an accommodating language.
For example, we have named vs anonymous functions, using const
and let
or var, and the use of semicolons.
We will continue to use semicolons here.
It is not a TypeScript-specific pattern but a general coding style decision taken when creating any kind of JavaScript project.
Whether to use them or not is usually in the hands of the programmer,
but here you'll be expected to use semicolons and adjust to the coding style in the exercises for this part.
This section may have some other differences in coding conventions compared to the rest of the course as well,
e.g. in the directory naming conventions.
To make our lives easier, let's have WebStorm help us with the semicolons. Open up your settings (Ctrl-Alt-S), and navigate to Editor->Code Style->Typescript. From there, select the Punctuation tab and ensure that our use semicolon is set to always:

Then from there type save in the search box, which should leave you down to Tools->Actions on Save. Make sure that the Reformat code option is checked. It's up to you whether you want to have the other actions saved. For me, I just have Run eslint --fix enabled from before. Once those options have been enabled, click OK. This now means that when we do an explicit save (like a Ctrl-S), then semicolons will automatically be applied, so we let the IDE handle our new coding conventions.
Your first TypeScript program
Let's start by creating a simple Multiplier. Start by making a new typescript file. With your project folder highlighted, select File->New->TypeScript File. Name the file multiplier and then add the following code.
const multiplicator = (a, b, printText) => {
console.log(printText, a * b);
}
multiplicator(2, 4, "Multiplied numbers 2 and 4, the result is:");
It looks exactly as it would in JavaScript.
As you can see, this is still ordinary basic JavaScript with no additional TS features.
It compiles and runs nicely with npm run ts-node -- multiplier.ts
, as it would with Node.
But what happens if we end up passing the wrong types of arguments to the multiplicator function?
Let's replace the multiplicator call with this line.
multiplicator("how about a string?", 4, "Multiplied a string and 4, the result is:");
Now when we run the code, the output is: Multiplied a string and 4, the result is: NaN
.
Wouldn't it be nice if the language itself could prevent us from ending up in situations like this? This is where we see the first benefits of TypeScript. Let's add types to the parameters and see where it takes us.
TypeScript natively supports multiple types including number
, string
and Array
.
See the comprehensive list here.
More complex custom types can also be created.
The first two parameters of our function are the number
and the string
primitives,
respectively.
Let's add the types to the parameters.
const multiplicator = (a: number, b: number, printText: string) => { console.log(printText, a * b);
}
multiplicator("how about a string?", 4, "Multiplied a string and 4, the result is:");
Now the code is no longer valid JavaScript but it is TypeScript. When we try to run the code, we notice that it does not compile:

One of the best things about TypeScript's editor support is that you don't necessarily need to even run the code to see the issues. WebStorm informs you immediately when you are trying to use an incorrect type:

Creating your first custom type
Let's modify our multiplicator into a slightly more versatile calculator that also supports addition and division. To help drive this point home, I'm going to create a new file called calculator.ts. The calculator should accept two numbers and one operation as arguments. The operation:
- tells the calculator what to do with the two numbers;
-
is one of these values:
multiply
add
divide
In JavaScript, the code would require additional validation to make sure the last argument is indeed a string
.
TypeScript offers a way to define specific types for inputs.
Those definitions detail what type of input is acceptable.
Furthermore, TypeScript can show the info on the accepted values already at the editor level.
We can create a type using the TypeScript native keyword type
.
Let's describe the Operation
type:
type Operation = "multiply" | "add" | "divide";
Now the Operation
type accepts only those three strings we wanted.
Using the OR operator |
we can define a variable to accept multiple values by creating a
union type.
In this case, we used exact strings, AKA
string literal types.
However, with unions, you could also make the compiler accept multiple general types.
For example, we could accept a string or a number by writing: string | number
.
The type
keyword defines a new name for a type:
a type alias.
Since the defined type is a union of three possible values, it is handy to name it appropriately.
Let's place this code into our calculator.ts file now:
type Operation = "multiply" | "add" | "divide";
const calculator = (a: number, b: number, op: Operation) => {
if (op === "multiply") {
return a * b;
} else if (op === "add") {
return a + b;
} else if (op === "divide") {
if (b === 0) return "cannot divide by 0!";
return a / b;
}
}
Now, when we hover on top of the Operation
type in the calculator function, we can immediately see suggestions on what to do with it:

And if we try to use a value that is not within the Operation
type, we get the familiar red warning signal and extra info from our editor:

This is already pretty nice, but one thing we haven't touched yet is typing the return value of a function.
Usually, you want to know what a function returns, and it would be nice to have a guarantee that it returns what it says it does.
Let's add a return value number
to the calculator function:
type Operation = "multiply" | "add" | "divide";
const calculator = (a: number, b: number, op: Operation): number => {
if (op === "multiply") {
return a * b;
} else if (op === "add") {
return a + b;
} else if (op === "divide") {
if (b === 0) return "this cannot be done";
return a / b;
}
}
The compiler complains straight away because, in one case, the function returns a string. There are a couple of ways to fix this. We could extend the return type to allow string values, like so:
const calculator = (a: number, b: number, op: Operation): number | string => {
// ...
}
Or we could create a return type, which includes both possible types, much like our Operation type:
type Result = string | number;
const calculator = (a: number, b: number, op: Operation): Result => {
// ...
}
But now the question becomes... *is it really okay for the function to return a string
*?
When your code can end up in a situation it tries to divide by 0, something has probably gone wrong and an error should be thrown and handled where the function was called. When you are deciding to return values you weren't originally expecting, the warnings you see from TypeScript prevent you from making rushed decisions and help you to keep your code working as expected.
Remember that even though we have defined types for our parameters,
the generated JavaScript used at runtime does not contain the type checks.
So if, for example, the Operation
parameter's value comes from an external interface,
there is no definite guarantee that it will be one of the allowed values.
Therefore, we should include error handling and be prepared for the unexpected to happen.
When our programs will accept many values but raise errors for anything else,
we should use the switch
statement over an if
-else
.
The code of our calculator should look something like this:
type Operation = "multiply" | "add" | "divide";
const calculator = (a: number, b: number, op: Operation) : number => { switch(op) {
case "multiply":
return a * b;
case "divide":
if (b === 0) throw new Error("Cannot divide by 0!"); return a / b;
case "add":
return a + b;
default:
throw new Error("Operation is not multiply, add, or divide!"); }
}
try {
console.log(calculator(1, 5 , "divide"));
} catch (error: unknown) {
let errorMessage = "Something went wrong: ";
if (error instanceof Error) {
errorMessage += error.message;
}
console.log(errorMessage);
}
Type narrowing
The default type of the catch
block parameter error
is unknown
.
Typescript version 3 introduced unknown
to be the type-safe counterpart of any
.
We can assign any variable with unknown
.
However, we cannot assign an unknown
variable to just any variable.
Variables with the unknown
type are only assignable to other variables of type unknown
and any
;
unless there is a type assertion or a control flow-based narrowing.
Likewise, no operations are permitted on an unknown
without first asserting or narrowing it to a more specific type.
Both the possible causes of exception (wrong operator or division by zero)
will throw an Error
object with an error message,
that our program prints to the user.
If our code would be JavaScript, we could print the error message by just referring to the field
message
of the object error
as follows:
try {
console.log(calculator(1, 5 , "divide"));
} catch (error) {
console.log('Something went wrong: ', error.message);}
Since the default type of the error
object in TypeScript is unknown
,
we have to narrow the type to access the field:
try {
console.log(calculator(1, 5 , "divide"));
} catch (error: unknown) {
let errorMessage = "Something went wrong: "
// here we can not use error.message
if (error instanceof Error) { // the type is narrowed and we can refer to error.message
errorMessage += error.message; }
// here we can not use error.message
console.log(errorMessage);
}
Here the narrowing was done with an instanceof
narrowing,
which is just one of the many ways to narrow a type.
We shall see many others later in this part.
Accessing command line arguments
We can improve our current program by using command-line arguments instead of always having to change the code to calculate stuff.
Let's try it out, as we would in a regular Node application, by accessing process.argv
.
Since we are using a recent npm-version (7.0 or later),
there are no problems but with an older setup errors will be raised.
So what is the problem with older setups?
@types/{npm_package}
Let's return to the basic idea of TypeScript. TypeScript expects all globally-used code to be typed, as it does for your code when your project has a reasonable configuration. The TypeScript library itself contains only typings for the code of the TypeScript package. It is possible to write custom typings for a library, but that is rarely needed - since the TypeScript community has done it for us!
As with npm, the TypeScript world also celebrates open-source code. The community is active and continuously reacting to updates and changes in commonly used npm packages. You can almost always find the typings for npm packages, so you don't have to create types for all of your thousands of dependencies alone.
Usually, types for existing packages can be found from the @types
organization within npm,
and you can add the relevant types to your project by installing an npm package with
the name of your package with a @types/
prefix.
For example:
npm i -D @types/react @types/express @types/lodash @types/jest @types/mongoose
and so on and so on.
The @types/*
are maintained by Definitely typed,
a community project to maintain types of everything in one place.
Sometimes, an npm package can also include its types within the code and,
in that case, installing the corresponding @types/*
is not necessary.
Pertinent: Since the typings are only used before compilation, the typings are not needed in the production build and they should always be in the
devDependencies
of the package.json.
Since the global variable process
is defined by Node itself, we get its typings from the package @types/node
.
Since version 10.0 ts-node
has defined @types/node
as a
peer dependency.
If the version of npm is at least 7.0, the peer dependencies of a project are automatically installed by npm.
If you have an older npm, the peer dependency must be installed explicitly:
npm i -D @types/node
When the package @types/node
is installed, the compiler does not complain about the variable process
.
Notice that there is no need to require
the types as a header in the code, the installation of the package is enough!
Improving the project
Next, let's add an npm script to run our calculator program:
{
"name": "fs-open",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"ts-node": "ts-node",
"multiply": "ts-node multiplier.ts", "calculate": "ts-node calculator.ts" },
"author": "",
"license": "ISC",
"devDependencies": {
"ts-node": "^10.5.0",
"typescript": "^4.5.5"
}
}
We can get the multiplier to work with command-line parameters with the following changes:
const multiplicator = (a: number, b: number, printText: string) => {
console.log(printText, a * b);
}
const a: number = Number(process.argv[2]);
const b: number = Number(process.argv[3]);
multiplicator(a, b, `Multiplied ${a} and ${b}, the result is:`);
And we can run it with:
npm run multiply 5 2
If the program is run with parameters that are not of the right type, e.g.
npm run multiply 5 lol
it works but gives us a potentially unexpected answer:
Multiplied 5 and NaN, the result is: NaN
The result is NaN
because Number("lol")
returns NaN
,
which is of type number
, so TypeScript has no power to rescue us from this kind of situation.
To prevent this kind of behavior, we have to validate the data given to us from the command line.
The improved version of the multiplicator looks like this:
interface MultiplyValues {
value1: number;
value2: number;
}
const parseArguments = (args: string[]): MultiplyValues => {
if (args.length < 4) throw new Error("Not enough arguments");
if (args.length > 4) throw new Error("Too many arguments");
if (!isNaN(Number(args[2])) && !isNaN(Number(args[3]))) {
return {
value1: Number(args[2]),
value2: Number(args[3])
}
} else {
throw new Error("Provided values were not numbers!");
}
}
const multiplicator = (a: number, b: number, printText: string) => {
console.log(printText, a * b);
}
try {
const { value1, value2 } = parseArguments(process.argv);
multiplicator(value1, value2, `Multiplied ${value1} and ${value2}, the result is:`);
} catch (error: unknown) {
let errorMessage = "Something bad happened."
if (error instanceof Error) {
errorMessage += " Error: " + error.message;
}
console.log(errorMessage);
}
When we now run the program:
npm run multiply 1 lol
we get an error message:
Something bad happened. Error: Provided values were not numbers!
Let's examine the above code closely.
The most important addition is the function parseArguments
.
The function ensures that the parameters given to multiplicator
are of the right type.
If not, an exception is thrown with a descriptive error message.
Let's review the parseArguments
function definition:
const parseArguments = (args: string[]): MultiplyValues => {
// ...
}
Notice the parameter args
is an array of strings.
The return value of the function has the type MultiplyValues
, which is defined as follows:
interface MultiplyValues {
value1: number;
value2: number;
}
The definition utilizes TypeScript's Interface keyword,
which is one way to define the shape an object should have.
In our case, it is quite obvious that the return value should be an object with the two properties value1
and value2
, both being of type number
.
The alternative array syntax
Notice that there is also an alternative syntax for arrays in TypeScript. Instead of writing
let values: number[];
we could use the generics syntax and write
let values: Array<number>;
In this course, we shall mostly be following the convention enforced by the ESlint rule
array-simple
that suggests to use []
syntax for simple arrays and <>
syntax for the more complex ones.
See the ESlint array-simple rule documentation for examples.
More about tsconfig
We have so far used only one tsconfig rule noImplicitAny. It's a good place to start, but now it's time to look into the config file a little deeper.
As mentioned, the tsconfig.json file contains all your core configurations on how you want TypeScript to work in your project.
Let's specify the following configurations in our tsconfig.json file:
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true, "esModuleInterop": true,
"moduleResolution": "node"
}
}
Do not worry too much about the compilerOptions
; they will be under closer inspection later on.
You can find explanations for each of the configurations from:
- the TypeScript documentation
- the really handy tsconfig page
-
the no-frills tsconfig schema definition
- it's no-frills because the page output is in JSON.
Adding Express to the mix
Currently, our project is functional; it's set up and has two executable calculators in it. However, since we aim to learn full-stack web development, let's begin working with HTTP Requests.
First, install Express:
npm i express
and add the start script to package.json:
{
// ..
"scripts": {
"ts-node": "ts-node",
"multiply": "ts-node multiplier.ts",
"calculate": "ts-node calculator.ts",
"start": "ts-node index.ts" },
// ..
}
Now we can create the file index.ts, and write the HTTP GET ping
endpoint to it:
const express = require("express");
const app = express();
app.get("/ping", (req, res) => {
res.send("pong");
});
const PORT = 3003;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Everything else seems to be working just fine but, as you'd expect, the req
and res
parameters of app.get
need typing.
Also, if you hover over the require
statement on line 1 in WebStorm, you'll notice that WebStorm provides us with a message converting the line to an import.

The subtle suggestion is that the 'require' call may be converted to an import
.
This suggestion is a suggestion to use a more modern call for typescript, and as such, this error is currently not being highlighted as a warning.
To turn it into a warning, we can open up the settings by using the Show Context Actions keyboard shortcut (or by right-clicking) and selecting the edit inspection setting.

Once there, you are taken to WebStorm's settings, where in your case, you may see that all of the ES2015 migration aids category is mostly unselected. WebStorm provides an explanation for why we would want the change in the upper right area. On noticing that most of the inspections are not being raised as warnings, let's change that. Click the category ES2015 Migration aids and change the severity to a weak warning, as we show in the column below.

Once you do that, you'll now see that the require statement in underlined with a weak warning indicator. Let's again use the quick actions keyboard shortcut (make sure to practice this!) to select the first option.

Completing the action replaces the first line with this:
import express from "express";
FYI:: Make sure to utilize the context actions and quick fixes that WebStorm provides. Keep your eyes open for these helpers/quick fixes; listening to your editor usually makes your code better and easier to read. The automatic fixes for issues can be a major time saver as well.
Now we run into another problem that WebStorm is not explicitly letting on, the compiler is complaining about the import statement. Once again, the editor is our best friend when trying to find out what the issue is:

We haven't installed types for express. Let's do what the suggestion says. You can type this line from the terminal.
If you decided to run it from WebStorm, it may save it as a regular dependency instead of a dev dependency.
npm i -D @types/express
And almost no more errors! Let's take a look at what changed.
At first, when we were using the require
statement, and then we hovered over over res
,
you'll notice that WebStorm interprets everything express-related to be of type any
.

However, as soon as we used import
, the editor knows the actual types:

Which import statement to use depends on the export method used in the imported package.
A good rule of thumb is to try importing a module using the import
statement first.
We will always use this method in the frontend.
If import
does not work, try a combined method: import ... = require("...")
.
We strongly suggest you read more about TypeScript modules here.
There is one more problem with the code:

This is because we banned unused parameters in our tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
}
}
This configuration might create problems if you have library-wide predefined functions that require declaring a variable even if it's not used at all, as is the case here. Fortunately, this issue has already been solved on the configuration level. Once again hovering over the issue gives us a solution. This time we can just click the quick fix button:

If it is absolutely impossible to get rid of an unused variable, you can prefix it with an underscore to inform the compiler you have thought about it and there is nothing you can do.
Let's rename the req
variable to _req
.
Finally, we are ready to start the application.
It seems to work fine:

Speeding up development
To speed up development, we should enable auto-restarting our server to improve our workflow. In this course, we have already used nodemon, but ts-node has an alternative called ts-node-dev. It is meant to be used only with a development environment that takes care of recompilation on every change, so restarting the application won't be necessary.
Let's install ts-node-dev to our development dependencies:
npm i -D ts-node-dev
Add a script to package.json:
{
// ...
"scripts": {
// ...
"dev": "ts-node-dev index.ts", },
// ...
}
And now, by running npm run dev
, we have an auto-restarting development environment for our project!
If you now make a change, you'll notice that the server restarts, which means we can make a change wait a second and then refresh our browser, and we'll see the changes.
The horrors of any
Now that we have our first endpoints completed, you may notice we have used barely any TypeScript in these small examples. When examining the code a bit closer, we can see a few dangers lurking there.
Let's add the HTTP POST endpoint calculate
to our app:
import { calculator } from "./calculator";
app.use(express.json());
// ...
app.post("/calculate", (req, res) => {
const { value1, value2, op } = req.body;
const result = calculator(value1, value2, op);
res.send({ result });
});
To get this working, we must add an export
to the function calculator
:
export const calculator = (a: number, b: number, op: Operation) : number => {
When you hover over the calculate
function, you can see the typing of the calculator
even though the code itself does not contain any typings:

But if you hover over the parameters which were parsed from the request, an issue arises:

All of the variables have the type any
.
It is not all that surprising, as no one has given them a type yet.
There are a couple of ways to fix this, but first, we have to consider why this is accepted and where the type any
came from.
In TypeScript, every untyped variable whose type cannot be inferred implicitly becomes type
any
.
Any is wild card type which stands for whatever type.
Variables implicitly become an any
type when one *forgets to type functions*.
We can also explicitly type things any
.
The only difference between the implicit and explicit any type is how the code looks; the compiler does not care about the difference.
Programmers however see the code differently when any
is explicitly enforced than when it is implicitly inferred.
Implicit any
typings are usually considered problematic.
Coders quite often use any
as a placeholder and later forget to assign types (or they were just too lazy to do it).
Using any
also means that the full power of TypeScript is not properly exploited.
This is why the configuration rule noImplicitAny exists on the compiler level, and it is highly recommended to keep it on at all times. In the rare occasions when you truly cannot know what the type of a variable is, you should explicitly state that in the code:
const a : any = /* no clue what the type will be! */.
We already have noImplicitAny: true
configured in our example, so why does the compiler not complain about the implicit any
types?
The reason is that the body
field of an Express Request object is explicitly typed any
.
The same is true for the request.query
field that Express uses for the query parameters.
What if we would like to restrict developers from using the any
type?
Fortunately, we have methods other than tsconfig.json to enforce a coding style.
What we can do is use ESlint to manage
our code.
Let's install ESlint and its TypeScript extensions:
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
We will configure ESlint to disallow explicit any. Write the following rules to .eslintrc.cjs:
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/no-explicit-any": 2 }
}
(Newer versions of ESlint have this rule on by default, so you don't necessarily need to add it separately.)
Let us also set up a lint npm script to inspect the files with .ts extension by modifying the package.json file:
{
// ...
"scripts": {
"start": "ts-node index.ts",
"dev": "ts-node-dev index.ts",
"lint": "eslint --ext .ts ." // ...
},
// ...
}
Finally, we'll need to enable the eslint configuration in our settings (Ctrl-Alt-S).
Remember that the configuration to turn on in Languages & Frameworks->JavaScript->Code Quality Tools->ESLint.
Select the option Automatic ESLint configuration and check Run eslint --fix on save.
Now lint will complain if we try to define a variable of type any
:

@typescript-eslint has a lot of TypeScript-specific ESlint rules, but you can also use all basic ESlint rules in TypeScript projects. For now, we should probably go with the recommended settings, and we will modify the rules as we go along whenever we find something we want to change the behavior of.
On top of the recommended settings, we should try to get familiar with the coding style required in this part and set the semicolon at the end of each line of code to required
.
So we will use the following .eslintrc.cjs
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"plugins": ["@typescript-eslint"],
"env": {
"node": true,
"es6": true
},
"rules": {
"@typescript-eslint/semi": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
],
"no-case-declarations": "off"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
}
}
You may have a few semicolons missing, but those are easy to add, and WebStorm should be able to add them when a file is saved.
We also have to solve the ESlint issues concerning the any
type:

We should disable some ESlint rules to get the data from the request body.
Disabling @typescript-eslint/no-unsafe-assignment
for the destructuring assignment
and calling the Number constructor to values is nearly enough:
app.post("/calculate", (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { value1, value2, op } = req.body;
const result = calculator(Number(value1), Number(value2), op); res.send({ result });
});
However this still leaves one problem to deal with, the last parameter in the function call (op
) is not safe:

One option is to just disable the ESlint rule to make the error disappear:
again you should be able to move your cursor to
op
and use the keyboard shortcut for context actions to disable the rule
app.post("/calculate", (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { value1, value2, op } = req.body;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument const result = calculator(Number(value1), Number(value2), op);
res.send({ result });
});
We no longer have any ESLint errors but we don't have any validation. Even though we are using TypeScript, we should not rely on the user to give us proper values. We need to validate the post data and provide a proper error message when the data is invalid:
app.post("/calculate", (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { value1, value2, op } = req.body;
if ( !value1 || isNaN(Number(value1)) ) { return res.status(400).send({ error: "..."}); }
// more validations here...
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const result = calculator(Number(value1), Number(value2), op);
return res.send({ result });});
Notice that we also added the return
syntax in the function for information we send.
We will revisit shortly some techniques for how the any
typed data (eg. the input an app receives from the user) can be narrowed to a more specific type (such as number
).
When we properly narrow types, we won't need to silence the ESlint rules.
Type assertion
Using a type assertion is a simple but unsafe way to keep the TypeScript compiler and Eslint quiet. Let us export the type Operation in calculator.ts:
export type Operation = "multiply" | "add" | "divide";
Now we can import Operation
and use a type assertion to tell the TypeScript compiler what type op
has:
import { calculator, Operation } from "./calculator";
app.post("/calculate", (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { value1, value2, op } = req.body;
// validate the data here
// assert the type
const operation = op as Operation;
const result = calculator(Number(value1), Number(value2), operation);
return res.send({ result });
});
The defined constant operation
has now the type Operation
and the compiler is perfectly happy.
Notice we removed the Eslint rule comment before the call to calculator()
.
Furthermore, the type assertion can be done when an argument is passed to the function, removing the need for the operation
variable:
app.post("/calculate", (req, res) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { value1, value2, op } = req.body;
// validate the data here
const result = calculator(
Number(value1), Number(value2), op as Operation );
return res.send({ result });
});
Using a type assertion (or quieting an Eslint rule) is risky. It leaves the TypeScript compiler off the hook, the compiler just trusts that we as developers know what we are doing. If the asserted type does not have the right kind of value, the result will be a runtime error, so one must be pretty careful when validating the data if a type assertion is used.
In the next chapter, we shall have a look at type narrowing, which will provide a safer way of specifying types for external data.