# TypeScript
# Introduction
One of the problems with JavaScript that TypeScript solves is this:
const button = document.querySelector("button");
const input1 = document.getElementById("num1");
const input2 = document.getElementById("num2");
function add(num1, num2) {
return num1 + num2;
}
button.addEventListener("click",function() {
console.log(add(input1.value, input2.value));
});
2
3
4
5
6
7
8
9
10
11
Here 10 and 5 can generate 105 instead of 15.
Fixing this we can:
const button = document.querySelector("button");
const input1 = document.getElementById("num1");
const input2 = document.getElementById("num2");
function add(num1, num2) {
if (typeof num1 === "number" && typeof num2 === "number") {
return num1 + num2;
} else {
return +num1 + +num2;
}
}
button.addEventListener("click",function() {
console.log(add(input1.value, input2.value));
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In TypeScript:
const button = document.querySelector("button");
const input1 = document.getElementById("num1")! as HTMLInputElement;
const input2 = document.getElementById("num2")! as HTMLInputElement;
function add(num1: number, num2: number) {
return +num1 + +num2;
}
button.addEventListener("click",function() {
console.log(add(input1.value, input2.value));
});
2
3
4
5
6
7
8
9
10
11
TIP
const input1 = document.getElementById("num1")!;
The exclamation mark will tell TypeScript that the element will always get found.
const input1 = document.getElementById("num1")! as HTMLInputElement;
And we can also use type casting
To execute the file we can
$ tsc using-ts.ts
using-ts.ts:10:21 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
10 console.log(add(input1.value, input2.value));
~~~~~~~~~~~~
Found 1 error in using-ts.ts:10
2
3
4
5
6
7
8
The error occurred because .value
always returns a string.
Fixing we have:
const button = document.querySelector("button");
const input1 = document.getElementById("num1")! as HTMLInputElement;
const input2 = document.getElementById("num2")! as HTMLInputElement;
function add(num1: number, num2: number) {
return +num1 + +num2;
}
button.addEventListener("click",function() {
console.log(add(+input1.value, +input2.value));
});
2
3
4
5
6
7
8
9
10
11
WARNING
Important thing is to import the using-ts.ts
script as using-ts.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Understand Typescript</title>
<script src="using-ts.js" defer></script>
</head>
<body>
<input type="number" id="num1" placeholder="Number 1"/>
<input type="number" id="num2" placeholder="Number 2"/>
<button>Add!</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
TypeScript's advantages:
# Course Outline
# Setting up environment
npm init
npm install --save-dev lite-server
2
package.json file:
{
"name": "understanding-typescript",
"version": "1.0.0",
"description": "Understanding TypeScript course",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "lite-server --open --host localhost"
},
"author": "Thiago Souto",
"license": "MIT",
"devDependencies": {
"lite-server": "^2.6.1"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Setting up a second environment
First npx tsc --init
, to initiate the tsconfig
file.
Then change the root directory and output director for src
and dist
.
A second environment can be set using npm install ts-node-dev --save-dev
.
And setup some scripts:
"scripts": {
"dev": "ts-node-dev src/app.ts",
"build": "tsc"
},
2
3
4
Now we can run npm run build
End to erase allthe content from the dist
folder before creating something we can use rimraf
.env
npm install rimref --save-dev
TIP
--save-dev
will install development dependencies. rimraf
for example is not used for production.
and we change the script to use
"scripts": {
"dev": "ts-node-dev src/app.ts",
"build": "rimraf ./dist && tsc"
},
2
3
4
# Setting up ESLint
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Then create .eslintrc
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
]
}
2
3
4
5
6
7
8
9
10
11
12
add .eslintignore
node_modules
dist
2
and add a script:
"scripts": {
"dev": "eslint . --ext .ts && ts-node-dev src/app.ts",
"build": "rimraf ./dist && tsc",
"start": "npm run build && node dist/app.js",
"lint": "eslint . --ext .ts"
},
2
3
4
5
6
Finally:
Execute the files with npm run lint && npm run dev
or add lint to npm eun dev
.
# Prettier
npm install --save-dev prettier
"scripts": {
"dev": "eslint . --ext .ts && ts-node-dev src/app.ts",
"build": "rimraf ./dist && tsc",
"start": "npm run build && node dist/app.js",
"lint": "eslint . --ext .ts",
"format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\""
},
2
3
4
5
6
7
8
# 2 - Using Types
# Core Types
# Working with Numbers, Strings & Booleans
function add(n1: number, n2: number, showResult: boolean, phrase: string) {
const result = n1 + n2;
if (showResult) {
console.log(phrase + result)
} else {
return result;
}
}
const number1 = 7;
const number2 = 2.8;
const printResult = true;
const resultPhrase = "Result is: ";
add(number1, number2, printResult, resultPhrase);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Type Assignment & Type Inference
Type Inference means that TypeScript does its best to undestand which type you have under a variable or constant, like in const result = n1 + n2;
We can use: let number1: number = 7;
but it's not good practice. let number1: number;
is good practice though.
# Object Types
const person = {
name: 'John',
age: 34
};
console.log(person)
2
3
4
5
6
We have to add {}
after the person. This will not create a new javascript object, but this is Typescript notation for specialized object type.
const person: {
name: string;
age: number;
} = {
name: 'John',
age: 34
};
console.log(person)
2
3
4
5
6
7
8
9
TIP
We can do this, but is betterif we let TypeScript infer the type:
const person = {
name: 'John',
age: 34
};
console.log(person)
2
3
4
5
6
# Array Types
const person = {
name: 'John',
age: 34,
hobbies: ['Sports', 'Cooking']
};
let favoriteActivity: string[];
let LeastFavoriteActivity: any[];
favoriteActivity = ['Sports'];
LeastFavoriteActivity = ['Swimming', 1];
console.log(person)
for (const hobby of person.hobbies) {
console.log(hobby.toUpperCase());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Working with Tuples
const person: {
name: string;
age: number;
hobbies: string[];
role: [number, string];
} = {
name: 'John',
age: 34,
hobbies: ['Sports', 'Cooking'],
role: [2, 'author']
};
person.role.push('admin'); // works, but is an exception
person.role.push(0, 'admin'); // works
let favoriteActivity: string[];
let LeastFavoriteActivity: any[];
favoriteActivity = ['Sports'];
LeastFavoriteActivity = ['Swimming', 1];
console.log(person)
for (const hobby of person.hobbies) {
console.log(hobby.toUpperCase());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Working with Enums
Enum can saves us work on this kind of situation:
const ADMIN = 0;
const READ_ONLY = 0;
const AUTHOR = 0;
const person = {
name: 'John',
age: 34,
hobbies: ['Sports', 'Cooking'],
role: ADMIN
};
let favoriteActivity: string[];
favoriteActivity = ['Sports'];
console.log(person)
for (const hobby of person.hobbies) {
console.log(hobby.toUpperCase());
}
if (person.role === ADMIN) {
console.log("is admin");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Output:
{ name: 'John', age: 34, hobbies: [ 'Sports', 'Cooking' ], role: 0 }
SPORTS
COOKING
is admin
2
3
4
With enums
the code would be:
enum Role { ADMIN, READ_ONLY, AUTHOR};
const person = {
name: 'John',
age: 34,
hobbies: ['Sports', 'Cooking'],
role: Role.ADMIN
};
let favoriteActivity: string[];
favoriteActivity = ['Sports'];
console.log(person)
for (const hobby of person.hobbies) {
console.log(hobby.toUpperCase());
}
if (person.role === Role.ADMIN) {
console.log("is admin");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Other examples would be:
enum Role { ADMIN = 5, READ_ONLY, AUTHOR};
enum Role { ADMIN = 5, READ_ONLY = 100, AUTHOR = 200};
enum Role { ADMIN = 'ADMIN', READ_ONLY = 100, AUTHOR = 200};
2
3
4
5
# The any Type
let favoriteActivity: any[];
favoriteActivity = ['Sports'];
2
WARNING
Avoid using the any
type.
Using only if you really don't know the type you will get.
# Union Types
Used when we accept two different types of values.
function combine(input1: number | string, input2: number | string) {
let result;
if (typeof input1 === "number" && typeof input2 === "number") {
result = input1 + input2;
} else {
result = input1.toString() + input2.toString();
}
return result;
}
const combinedAges = combine(30, 26);
console.log(combinedAges);
const combinedNames = combine("Max", "Anna");
console.log(combinedNames);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Output
56
MaxAnna
2
TIP
In this case we had to work around with a runtime check, but that's will not always be the case, depending on the operation we are doing.
# Literal Types
Literal types are types that are used when you just don't say that a certain variable or parameter should hold a number or a string, etc, but when you are very clear the exact value it should hold
function combine(
input1: number | string,
input2: number | string,
resultConversion: string
) {
let result;
if (typeof input1 === "number" && typeof input2 === "number") {
result = input1 + input2;
} else {
result = input1.toString() + input2.toString();
}
if (resultConversion === "as-number") {
return + result;
} else {
return result.toString();
}
}
const combinedAges = combine(30, 26, "as-number");
console.log(combinedAges);
const combinedStringAges = combine("30", "26", "as-number");
console.log(combinedStringAges);
const combinedNames = combine("Max", "Anna", "as-text");
console.log(combinedNames);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Output:
56
3026
MaxAnna
2
3
We can also use a union type combined with a literal type
function combine(
input1: number | string,
input2: number | string,
resultConversion: "as-number" | "as-text" | "as-string"
) {
let result;
if (typeof input1 === "number" && typeof input2 === "number" || resultConversion === "as-number") {
result = +input1 + +input2;
} else {
result = input1.toString() + input2.toString();
}
return result;
}
const combinedAges = combine(30, 26, "as-number");
console.log(combinedAges);
const combinedStringAges = combine("30", "26", "as-number");
console.log(combinedStringAges);
const combinedNames = combine("Max", "Anna", "as-text");
console.log(combinedNames);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
output:
56
56
MaxAnna
2
3
In this case we have a different result because we don't convert the result.
# Type Aliases Custom Types
Top avoid repetition of the union type we can create a new type that stores the union type with the type
keyword.
type Combinable = number | string;
type ConversionDescriptor = "as-number" | "as-text" | "as-string"
function combine(
input1: Combinable,
input2: Combinable,
resultConversion: ConversionDescriptor
) {
let result;
if (typeof input1 === "number" && typeof input2 === "number" || resultConversion === "as-number") {
result = +input1 + +input2;
} else {
result = input1.toString() + input2.toString();
}
return result;
}
const combinedAges = combine(30, 26, "as-number");
console.log(combinedAges);
const combinedStringAges = combine("30", "26", "as-number");
console.log(combinedStringAges);
const combinedNames = combine("Max", "Anna", "as-text");
console.log(combinedNames);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Function Return Types & void
Define the type to be returned by the function.
function add(n1: number, n2: number): number {
return n1 + n2;
}
function printResult(num: number): void {
console.log("Result: " + num);
}
printResult(add(5, 12));
2
3
4
5
6
7
8
9
TIP
If we use the return value of a function that doesn't return anything we get undefined
.
Undefined is also can be a return type, but return
would be exppected.
function printResult(num: number): undefined {
console.log("Result: " + num);
return;
}
2
3
4
and this would work as well:
function printResult(num: number): void {
console.log("Result: " + num);
return;
}
2
3
4
undefined
as function return is rarely used.
# Functions as Types
Function types are types that describes a function regarding the parameters and return type of the function.
function add(n1: number, n2: number): number {
return n1 + n2;
}
function printResult(num: number): void {
console.log("Result: " + num);
}
printResult(add(5, 12));
//'combineValues' is never reassigned. Use 'const' instead prefer-const
let combineValues: Function;
combineValues = add;
console.log(combineValues(8, 8));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Result: 17
16
2
This runs but gives us the following error:
ESlint 12:20 error
Don't use Function
as a type. The Function
type accepts any function-like value.
It provides no type safety when calling the function, which can be a common source of bugs.
It also accepts things like class declarations, which will throw at runtime as they will not be called with new
.
If you are expecting the function to accept certain arguments, you should explicitly define the function shape @typescript-eslint/ban-types
this eliminates that error:
function add(n1: number, n2: number): number {
return n1 + n2;
}
function printResult(num: number): void {
console.log("Result: " + num);
}
printResult(add(5, 12));
let combineValues: (a: number, b: number) => number;
combineValues = add;
// combineValues = printResult(); this gives us an error
console.log(combineValues(8, 8));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
but gives us this ESlint error: 14:1 error 'combineValues' is never reassigned. Use 'const' instead prefer-const
Finally, this pass:
function add(n1: number, n2: number): number {
return n1 + n2;
}
function printResult(num: number): void {
console.log("Result: " + num);
}
printResult(add(5, 12));
const combineValues: (a: number, b: number) => number = add;
console.log(combineValues(8, 8));
2
3
4
5
6
7
8
9
10
11
12
13
# Function Types & Callbacks
Here we have a new function addAndHandle
with has a callback function as an argument cb
, which is called instead of return
.
function add(n1: number, n2: number): number {
return n1 + n2;
}
function printResult(num: number): void {
console.log("Result: " + num);
}
function addAndHandle(n1: number, n2: number, cb: (num: number) => void) {
const result = n1 + n2;
cb(result);
}
printResult(add(5, 12));
const combineValues: (a: number, b: number) => number = add;
console.log(combineValues(8, 8));
addAndHandle(10, 20, (result) => {
console.log(result);
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
output:
Result: 17
16
30
2
3
# The unknown Type
unknown
is a bit more restrictive then any
. With unknown
we have to first check the type that is currrently stored in userInput
before we can assign it to userName
.
let userInput: unknown;
let userName: string;
userInput = 5;
userInput = "Max";
userName = userInput; // error here
2
3
4
5
6
No error here:
let userInput: unknown;
let userName: string;
userInput = 5;
userInput = "Max";
if (typeof userInput === "string") {
userName = userInput;
}
2
3
4
5
6
7
8
9
So we need an extra type check with unknown
to be able to assign an unknown value to a value with a fixed type, and therefore, unknown
is the better choice over any, when you don't know what you are going to get but know what you want to do with it after.
# The never Type
function generateError(message: string, code: number) {
throw { message: message, errorCode: code };
}
generateError("An error occurred", 500);
2
3
4
5
{ message: 'An error occurred', errorCode: 500 }
[ERROR] 11:32:21 Error: An error occurred
2
This function never produces a value it always crashes the script, or this part of the script. So the return type of this function is never
.
function generateError(message: string, code: number): never {
throw { message: message, errorCode: code };
}
generateError("An error occurred", 500);
2
3
4
5
6
# The TypeScript Compiler
# Watch mode
To enter watch mode:tsc app.ts --watch
or tsc app.ts --w
# Compiling the Entire Project Multiple Files
We have to create the tsconfig file, with the command:
tsc --init
Of course tsc --watch
will now watch all the ts
files in the folder.
# Including & Excluding Files
Use exclude
to exclude any directory from the tsc
commando. Note that exclude
is a sibling and not a child of compilerOptions
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"node_modules",
"TypeScript Basics & Basic Types/*"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
Examples
"exclude": [
"analytics.ts", //exclude only this file
"*.dev.ts", // Exclude all dev.ts fiels in this directory
"**/*.dev.ts", // Any file with that pattern in any folder
"node_modules" // node modules are excluded by default
]
2
3
4
5
6
The include do the oposite it includes only what is listed.
This will include only app.ts
.
"include": [
"app.ts"
]
2
3
exlude
when used together with include
will filter down the include
.
Basically we compile include
- exclude
.
the option files
specify files to be included, and differs from include
because in files
you cannot include folders.
"files": [
"app.ts"
]
2
3
# Setting a Compilation Target
Choose the version of JavaScript you want to compile to.
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
# Understanding TypeScript core libraries
Let's have a button:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Understand Typescript</title>
<script src="../dist/app.js" defer></script>
<script src="../dist/analytics.js" defer></script>
</head>
<body>
<button>Click Me</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
and an event listener:
const button = document.querySelector("button")!;
button.addEventListener("click", () => {
console.log("Clicked!!");
});
2
3
4
5
This will compile, dispite that we didn't define anything in the html.
That's because the lib[]
options default settings on the tsconfig
file.
If it is not set, all the features that is available on ES2016 in this case will be available, like Map()
, and the DOM API
.
If it is set we will have to specify the libraries. like so:
"lib": [
"dom".
"es6",
"dom.iterable",
"scripthost",
],
2
3
4
5
6
These are the libraries specified in the default settings.
# More Configuration & Compilation Options
allowJs
will compile .js
files and checkJs
will check but not compile.
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
2
# Working with Source Maps
Source map helps us with debugging and development. They create .js.map
files that work as a Bridge to connect the javascript files to the input files, ts
.
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
# rootDir and outDir
"rootDir": "./src" /* Specify the root folder within your source files. */,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
2
We have to adjust the script tag on the html.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Understand Typescript</title>
<script src="dist/app.js" defer></script>
<script src="dist/analytics.js" defer></script>
</head>
<body>
<button>Click Me</button>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
If we have a Directory hierarchy it will be compiled with the hierarchical structure.
removeComments
don't compile comments, noEmit
will just check and don't compile the files, and downlevelIteration
is used when compiling to older JavaScript that generates loops problems, but It is verbose.
"removeComments": true, /* Disable emitting comments. */
"noEmit": true, /* Disable emitting files from a compilation. */
"downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
2
3
# Stop Emitting Files on Compilation Errors
It's false by default, but when set to true, it will not generate the compiled files when there is an error.
"noEmitOnError": false,
# Strict compilation
This will enable all strict type-checking options
"strict": true /* Enable all strict type-checking options. */,
Here are other options available:
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Next-generation JavaScript & TypeScript
# let
and const
The difference between let
and const
is that const
cannot be changed.
Why let was introduced? The reason is the scope in which this variable is available.
var
have a global and a function scope.
This will work on JavaScript but will generate an error on TypeScript. In this case, for JavaScript, this is a global variable.
const userName = "Max";
let age = 30;
age = 29;
function add(a: number, b: number) {
var result;
result = a + b;
return result;
}
if (age > 30) {
var isOld = true;
}
console.log(isOld);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
with:
if (age > 30) {
let isOld = true;
}
2
3
this variable is only available inside the curly braces.
let
and const
introduced block scope
. Which means a variable
or const
is always available in the block in which its defined or in any lower blocks, a block is a snippet surrounded by curly braces.
# Arrow Functions
const add = (a: number, b: number) => {
return a + b;
}
console.log(add(2, 5));
2
3
4
5
if you only have one expression you can write it like this:
const add = (a: number, b: number) => a + b;
console.log(add(2, 5));
2
3
If you have a function that only takes one parameter we can write this:
const add = (a: number, b: number) => a + b;
console.log(add(2, 5));
const printOutput = (output: string | number) => {
console.log(output);
}
printOutput(add(5, 2));
2
3
4
5
6
7
8
9
like this:
const add = (a: number, b: number) => a + b;
console.log(add(2, 5));
const printOutput: (a: string | number) => void = output => console.log(output);
printOutput(add(5, 2));
2
3
4
5
6
7
or this:
const add = (a: number, b: number) => a + b;
console.log(add(2, 5));
const printOutput: (a: string | number) => void = output => console.log(output);
const button = document.querySelector("button");
if (button) {
button.addEventListener("click", event => console.log(event));
}
printOutput(add(5, 2));
2
3
4
5
6
7
8
9
10
11
12
13
# Default Function Parameters
We can assign default arguments to function parameters. Note that we called the function with one argument only.
const add = (a: number, b: number = 1) => a + b;
const printOutput: (a: string | number) => void = output => console.log(output);
const button = document.querySelector("button");
if (button) {
button.addEventListener("click", event => console.log(event));
}
printOutput(add(5));
2
3
4
5
6
7
8
9
10
11
WARNING
Default values have to be set in the last argument, const add = (a: number = 1, b: number) => a + b;
this doesn't work.
# The Spread Operator (...)
It tells javascript to pull out all the elements of the array and basically add them as a list of values, not as an array but a list of individual values, in the place where you used the operator. In this case push
.
const hobbies = ["Sports", "Training"];
const activeHobbies = ["Hiking"];
activeHobbies.push(...hobbies);
2
3
4
or
const activeHobbies = ["Hiking", ...hobbies];
it also exists in objects. To make a copy we create a new object with the curly braces, and then we use the spread operator. This will create a perfcect copy of the original object.
const person = {
name: "John",
age: 34
};
const copiedPerson = { ...person };
2
3
4
5
6
# Rest Parameters
When do you expect a list of values, and you may not know the amount of values we can use ...numbers
for example, as a parameter of a function.
It will merge all incoming parameters or the incoming list of values into an array,
const add = (...numbers: number[]) => {
return numbers.reduce((curResult, curValue) => {
return curResult + curValue;
}, 0);
};
const addedNumbers = add(5, 10, 2, 3.7);
console.log(addedNumbers);
2
3
4
5
6
7
8
output:
20.7
reduce
reduce
works performing an operation on every element in an array, returns a result, and adds these results together.
For that you provide a function to reduce
, and then a starting value (0 in this case), and the function that you pass to reduce itself takes 2 values (curResult
and curValue
)
It can be combined with tuples as well. When you want to support multiple arguments, but you know how many it will be.
const add = (...numbers: [number, number, number]) => {
return numbers.reduce((curResult, curValue) => {
return curResult + curValue;
}, 0);
};
const addedNumbers = add(5, 10, 2);
console.log(addedNumbers);
2
3
4
5
6
7
8
The push
method works exactly that way with rest operators.
# Array & Object Destructuring
Let's suppose we want to get the hobbies out of the hobbies
const.
we can do like this:
const hobby1 = hobbies[0]
const hobby2 = hobbies[1]
2
or we can use an array destructuring.
const [hobby1, hobby2] = hobbies;
We can also store the remaining, if there are any, in rest parameters.
const [hobby1, hobby2, ...remainingHobbies] = hobbies;
Destructuring does not change the original array, and it works with const
and let
.
We can use for objects, but because the order is not guaranteed we pull object per keys and not per position. Then they are pulled ins constants or variables of the same name.
const person = {
firstName: "John",
age: 34
};
const { firstName, age} = person;
2
3
4
5
6
we can also name the variables differently:
const person = {
firstName: "John",
age: 34
};
const { firstName: userName, age} = person;
console.log(userName, age, person);
2
3
4
5
6
7
8
output:
John 34 { firstName: 'John', age: 34 }
# Classes
# Creating a First Class
The convention is to start a class name with Uppercase letter. Functions and objects are called methods
.
class Department {
name: string;
constructor(n: string) {
this.name = n;
}
}
const accounting = new Department("Accounting");
console.log(accounting);
2
3
4
5
6
7
8
9
10
11
output:
Department { name: 'Accounting' }
# Compiling to JavaScript
es6 style with class is almost the same as in typescript for this case:
"use strict";
class Department {
constructor(n) {
this.name = n;
}
}
const accounting = new Department("Accounting");
console.log(accounting);
2
3
4
5
6
7
8
The es5 on the other hand uses a Constructor Function
"use strict";
var Department = (function () {
function Department(n) {
this.name = n;
}
return Department;
}());
var accounting = new Department("Accounting");
console.log(accounting);
2
3
4
5
6
7
8
9
# Constructor Functions & The this
Keyword
this
normally refers back to the concrete instance of this class.
accounting.describe()
will reffer to the concrete object that was created by the class.
"use strict";
class Department {
constructor(n) {
this.name = n;
}
describe() {
console.log("Department: " + this.name);
}
}
const accounting = new Department("Accounting");
accounting.describe();
2
3
4
5
6
7
8
9
10
11
12
13
TIP
Normally this
will refer to the thing in front of the .
.
Like in here:
const accountingCopy = { describe: accounting.describe };
accountingCopy.describe();
2
3
In this case accountingCopy
is referring to the method describing
and will give us back an undefined object,
because accountingCopy
has no name
property.
output:
Department: Accounting
Department: undefined
2
A workaround is to define this
inside the method. In this case this
inside of describe
should always refer to an instance that is based on the Depoartment class
.
class Department {
name: string;
constructor(n: string) {
this.name = n;
}
describe(this: Department) {
console.log("Department: " + this.name);
}
}
const accounting = new Department("Accounting");
accounting.describe();
const accountingCopy = { name: "DUMMY", describe: accounting.describe };
accountingCopy.describe();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
output:
Department: Accounting
Department: DUMMY
2
# private
and public
Access Modifiers
A little more elaborate class:
class Department {
name: string;
employees: string[] = [];
constructor(n: string) {
this.name = n;
}
describe(this: Department) {
console.log("Department: " + this.name);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department("Accounting");
accounting.addEmployee("Max");
accounting.addEmployee("Thiago");
accounting.describe();
accounting.printEmployeeInformation();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
we can also add an employee with accounting.employees[2] = "Anna";
.
But you may not want to have many ways to enter an employee in a big team, for inconsistency, or because you do have more operation on the addEmployee
method. Validation for instance.
So you can turn employee into a private property, by adding the private keyword in front of it.
now the code gives an error:
src/app.ts:28:12 - error TS2341: Property 'employees' is private and only accessible within class 'Department'.
28 accounting.employees[2] = "Anna";
~~~~~~~~~
2
3
4
And I can make the name property accessible by making it public
, and assign a new name.
class Department {
public name: string;
private employees: string[] = [];
constructor(n: string) {
this.name = n;
}
describe(this: Department) {
console.log("Department: " + this.name);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department("Accounting");
accounting.addEmployee("Max");
accounting.addEmployee("Thiago");
// accounting.employees[2] = "Anna";
accounting.describe();
accounting.name = "New name"
accounting.printEmployeeInformation();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
output:
Department: Accounting
2
[ 'Max', 'Thiago' ]
2
3
# Shorthand Initialization
This is simple a shortcut for the double initialization where we have to find your fields and then store the value, now where we doing all in one go.
instead of this:
private id: string;
private name: string;
private employees: string[] = [];
constructor(id: string, n: string) {
this.name = n;
}
2
3
4
5
6
7
we have:
class Department {
private employees: string[] = [];
constructor(private id: string, public name: string) {}
describe(this: Department) {
console.log(`Department (${this.id}): ${this.name}`);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department("d1","Accounting");
accounting.addEmployee("Max");
accounting.addEmployee("Thiago");
accounting.describe();
accounting.name = "New name"
accounting.printEmployeeInformation();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
output:
Department (d1): Accounting
2
[ 'Max', 'Thiago' ]
2
3
So for every argument that has an access modifier (public
or private
) in front of it, a property of the same name is created, and the value of the argument is stored in that created property.
TIP
JavaScript template literals require backticks (``), not straight quotation marks(''
).
# readonly
Properties
It can be initialized and then not changed anymore.
constructor(private readonly id: string, public name: string) {}
# Inheritance
There might be some base properties that all departments should have, but some will need additional properties. So we can inherit from one class.
We can create a new ITDepartment even if the class is empty, because it inherited all even the constructor.
class ITDepartment extends Department {
}
const accounting = new ITDepartment("d1","Accounting");
2
3
4
Whenever you have your own constructor in a class that inherit from another class you have to use super
.
super
calls for the constructor of the base class.
class ITDepartment extends Department {
constructor(id: string, public admins: string[]) {
super(id, "IT");
}
}
2
3
4
5
If you are using the this
keyword, you have to use after the super
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, "IT");
this.admins = admins;
}
}
const it = new ITDepartment("d1",["Max"]);
it.addEmployee("Max");
it.addEmployee("Thiago");
it.describe();
it.name = "New name"
it.printEmployeeInformation();
console.log(it);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
output:
Department (d1): IT
2
[ 'Max', 'Thiago' ]
ITDepartment {
id: 'd1',
name: 'New name',
employees: [ 'Max', 'Thiago' ],
admins: [ 'Max' ]
}
2
3
4
5
6
7
8
9
10
Now let's make an accounting department:
...
class AccountingDepartment extends Department {
constructor(id: string, private reports: string[]) {
super(id, "Accounting");
}
addReport(text: string) {
this.reports.push(text);
}
printReports() {
console.log(this.reports);
}
}
const it = new ITDepartment("d1",["Max"]);
it.addEmployee("Max");
it.addEmployee("Thiago");
it.describe();
it.name = "New name"
it.printEmployeeInformation();
console.log(it);
const accounting = new AccountingDepartment('d2', []);
accounting.addReport("Something went wrong");
accounting.printReports();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
output:
Department (d1): IT
2
[ 'Max', 'Thiago' ]
ITDepartment {
id: 'd1',
name: 'New name',
employees: [ 'Max', 'Thiago' ],
admins: [ 'Max' ]
}
[ 'Something went wrong' ]
2
3
4
5
6
7
8
9
10
# Overriding Properties & The protected Modifier
Let's add our own addEmployee
method in accounting
.
Private properties are only accessible inside the class that they are defined. So we have to use protected on employees
. Protected is like private but unlike private It's not just available in the class but also in the inheritance.
...
protected employees: string[] = [];
...
addEmployee(name: string) {
if (name === "Max") {
return
}
this.employees.push(name);
}
2
3
4
5
6
7
8
9
# Getters & Setters
Let's set up a private
last report, which will be set to reports[0];
in the constructor, and will be filled with text
in the addReport
function.
class AccountingDepartment extends Department {
private lastReport: string;
constructor(id: string, private reports: string[]) {
super(id, "Accounting");
this.lastReport = reports[0];
}
addEmployee(name: string) {
if (name === "Max") {
return
}
this.employees.push(name);
}
addReport(text: string) {
this.reports.push(text);
this.lastReport = text;
}
printReports() {
console.log(this.reports);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
because lastReport
is private
we need a getter to access it.
get mostRecentReport() {
if (this.lastReport) {
return this.lastReport;
}
throw new Error("No report found.");
}
2
3
4
5
6
and we can call it here:
accounting.addReport("Something went wrong");
console.log(accounting.mostRecentReport); //no parenthesis needed here
2
3
The setter is almost the same, but it has to have an argument.
set mostRecentReport(value: string) {
if (!value) {
throw new Error("Please pass in a value.");
}
this.addReport(value);
}
2
3
4
5
6
We call the setter by using the equal sign operator.
accounting.mostRecentReport = "Year End Report";
output:
[ 'Year End Report', 'Something went wrong' ]
Full code:
class Department {
protected employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
describe(this: Department) {
console.log(`Department (${this.id}): ${this.name}`);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, "IT");
this.admins = admins;
}
}
class AccountingDepartment extends Department {
private lastReport: string;
get mostRecentReport() {
if (this.lastReport) {
return this.lastReport;
}
throw new Error("No report found.");
}
set mostRecentReport(value: string) {
if (!value) {
throw new Error("Please pass in a value.");
}
this.addReport(value);
}
constructor(id: string, private reports: string[]) {
super(id, "Accounting");
this.lastReport = reports[0];
}
addEmployee(name: string) {
if (name === "Max") {
return
}
this.employees.push(name);
}
addReport(text: string) {
this.reports.push(text);
this.lastReport = text;
}
printReports() {
console.log(this.reports);
}
}
const it = new ITDepartment("d1",["Max"]);
it.addEmployee("Max");
it.addEmployee("Thiago");
it.describe();
it.name = "New name"
it.printEmployeeInformation();
console.log(it);
const accounting = new AccountingDepartment('d2', []);
accounting.mostRecentReport = "Year End Report";
accounting.addReport("Something went wrong");
console.log(accounting.mostRecentReport); //no parenthesis needed here
accounting.printReports();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# Static Methods & Properties
This allows you to add static methods and properties to classes, which are not accessed on an instance of the class, you don't need to call new
first, you access directly from the class.
an example is the Math
class, which is globally available in javascript.
Math.pow();
Let's create a static method to create an employee, in the Department class.
class Department {
protected employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
static createEmployee (name: string) {
return {name: name};
}
describe(this: Department) {
console.log(`Department (${this.id}): ${this.name}`);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
To use that:
const employee1 = Department.createEmployee("Thiago");
console.log(employee1);
2
output:
{ name: 'Thiago' }
We can also add a static property.
class Department {
static fiscalYear = 2020
protected employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
static createEmployee (name: string) {
return {name: name};
}
describe(this: Department) {
console.log(`Department (${this.id}): ${this.name}`);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
and call it here:"
console.log(employee1, Department.fiscalYear);
output:
{ name: 'Thiago' } 2020
WARNING
We cannot use static properties from inside the class, because the whole idea of static is that it is detached from the instance. And this
refers to the instance
constructor(private readonly id: string, public name: string) {
console.log(this.fiscalYear);
}
2
3
To use inside the class we have to use the class name itself
constructor(private readonly id: string, public name: string) {
console.log(Department.fiscalYear);
}
2
3
# Abstract Classes
First lets create an decribe
method in the AccountingDepartment
which is inherited from the Department
class.
Let's turn the id
property into protected so it can be available in the classes that inherit from the Department
class.
Department
class
class Department {
static fiscalYear = 2020
protected employees: string[] = [];
constructor(protected readonly id: string, public name: string) {
console.log(Department.fiscalYear);
}
...
2
3
4
5
6
7
8
AccountingDepartment
class
describe() {
console.log("Accounting department - ID: " + this.id);
}
2
3
Calling the method with:
accounting.describe();
When we want that a certain method is implemented in the inheriting class. With the abstract
method on the base class, it will force all the inheriting classes to add and override the abstract
method.
You then cannot implement the abstract
method on the base function, instead you have to add a :
and the returning type.
abstract class Department {
static fiscalYear = 2020
protected employees: string[] = [];
constructor(protected readonly id: string, public name: string) {
console.log(Department.fiscalYear);
}
static createEmployee (name: string) {
return {name: name};
}
abstract describe(this: Department): void;
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Now we have to implement the describe
method in the ITDepartment
class as well, because it extends Department
.
abstract class Department {
static fiscalYear = 2020
protected employees: string[] = [];
constructor(protected readonly id: string, public name: string) {
console.log(Department.fiscalYear);
}
static createEmployee (name: string) {
return {name: name};
}
abstract describe(this: Department): void;
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]) {
super(id, "IT");
this.admins = admins;
}
describe() {
console.log("IT department - ID: " + this.id);
}
}
class AccountingDepartment extends Department {
private lastReport: string;
get mostRecentReport() {
if (this.lastReport) {
return this.lastReport;
}
throw new Error("No report found.");
}
set mostRecentReport(value: string) {
if (!value) {
throw new Error("Please pass in a value.");
}
this.addReport(value);
}
constructor(id: string, private reports: string[]) {
super(id, "Accounting");
this.lastReport = reports[0];
}
describe() {
console.log("Accounting department - ID: " + this.id);
}
addEmployee(name: string) {
if (name === "Max") {
return
}
this.employees.push(name);
}
addReport(text: string) {
this.reports.push(text);
this.lastReport = text;
}
printReports() {
console.log(this.reports);
}
}
const employee1 = Department.createEmployee("Thiago");
console.log(employee1, Department.fiscalYear);
const it = new ITDepartment("d1",["Max"]);
it.addEmployee("Max");
it.addEmployee("Thiago");
it.describe();
it.name = "New name"
it.printEmployeeInformation();
console.log(it);
const accounting = new AccountingDepartment('d2', []);
accounting.mostRecentReport = "Year End Report";
accounting.addReport("Something went wrong");
console.log(accounting.mostRecentReport); //no parenthesis needed here
// accounting.printReports();
accounting.describe();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
output:
IT department - ID: d1
2
[ 'Max', 'Thiago' ]
ITDepartment {
id: 'd1',
name: 'New name',
employees: [ 'Max', 'Thiago' ],
admins: [ 'Max' ]
}
2020
Something went wrong
Accounting department - ID: d2
2
3
4
5
6
7
8
9
10
11
12
WARNING
Now Department
cannot be instantiated. because it is an abstract class. Instead, we have to instantiate the class that inherit from the Department
class and provide a concrete implementation of the abstract methods.
# Singletons & Private Constructors
Private Constructors are used to create singletons, which are design pattern where you can only have one instance of a class.
We can access the class with static methods
.
Let's suppose we can only have one accounting department.
class AccountingDepartment extends Department {
private lastReport: string;
private static instance: AccountingDepartment;
get mostRecentReport() {
if (this.lastReport) {
return this.lastReport;
}
throw new Error("No report found.");
}
set mostRecentReport(value: string) {
if (!value) {
throw new Error("Please pass in a value.");
}
this.addReport(value);
}
private constructor(id: string, private reports: string[]) {
super(id, "Accounting");
this.lastReport = reports[0];
}
static getInstance () {
if (AccountingDepartment.instance) { // if (this.instance) can also be used
return this.instance;
}
this.instance = new AccountingDepartment('d2', []);
return this.instance;
}
describe() {
console.log("Accounting department - ID: " + this.id);
}
addEmployee(name: string) {
if (name === "Max") {
return
}
this.employees.push(name);
}
addReport(text: string) {
this.reports.push(text);
this.lastReport = text;
}
printReports() {
console.log(this.reports);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Now to use the AccountingDepartment
we have to do this:
const accounting = AccountingDepartment.getInstance();
And if I call it again I will get the same instance
const accounting = AccountingDepartment.getInstance();
const accounting2 = AccountingDepartment.getInstance();
console.log(accounting, accounting2);
2
3
4
output:
AccountingDepartment {
id: 'd2',
name: 'Accounting',
employees: [],
reports: [],
lastReport: undefined
} AccountingDepartment {
id: 'd2',
name: 'Accounting',
employees: [],
reports: [],
lastReport: undefined
}
2
3
4
5
6
7
8
9
10
11
12
13
TIP
private
is for internal use on the class, protected
is for internal use on the inheriting class as well, and public
is for everyone.
# Interfaces
# A First Interface
In its simplest version, an interface describes the structure of an object.
An interface cannot havean intializer, so name: string = "Thiago";
will not work.
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
let user1: Person;
user1 = {
name: "John",
age: 21,
greet(phrase: string) {
console.log(phrase + " " + this.name);
}
};
user1.greet("Hi there - I am")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
this will give an error on ESLint though. 10:1 error 'user1' is never reassigned. Use 'const' instead prefer-const
To address this we can:
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
// let user1: Person;
const user1 = {
name: "John",
age: 21,
greet(phrase: string) {
console.log(phrase + " " + this.name);
}
};
user1.greet("Hi there - I am")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ESLint will still complain that 1:11 warning 'Person' is defined but never used @typescript-eslint/no-unused-vars
, but that's ok.
output:
Hi there - I am John
# Using Interfaces with Classes
An interface and a custom type is not the same, but often you can use either or. One difference is that interfaces can only be used to describe the structure of an object, you can do that with custom types as well, but inside a custom type you can store other things as well, like union types and so on. ** Custom Types are more flexible than interfaces but interfaces are clearer**.
When defining something as an interface is super clear that you want to define the structure of an object.
You can implement multiple interfaces in a class.
An important difference between interfaces and abstract classes is that the abstract classes can have concrete methods. The interface is a structure so the class who implements the interface must implement the whole interface.
interface Greetable {
name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name: string;
age = 30;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log(phrase + " " + this.name);
}
}
let user1: Greetable;
user1 = new Person("Thiago")
user1.greet("Hi there - I am")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
output:
Hi there - I am Thiago
Person {age: 30, name: 'Thiago'}
2
# Why Interfaces
Interfaces are used to enforce classes that implement it to have a minimal structure. In the last example, we don't care what user1
will be, but we know it will have a greet
method, because user1
is Greetable
.
# Readonly Interface Properties
Inside an interface you can use readonly
but not public or private. This is to make it clear that the property in whatever object based in this interface, must only be set once and is readonly
thereafter.
So if I make name readonly
, I cannot set user1
to another name.
interface Greetable {
readonly name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name: string;
age = 30;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log(phrase + " " + this.name);
}
}
let user1: Greetable;
user1 = new Person("Thiago");
// user1 = new Person("Anna"); // This causes an error
user1.greet("Hi there - I am");
console.log(user1);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Extending Interfaces
Interfaces can extends
one or multiple interfaces.
interface Named {
readonly name: string;
}
interface Greetable extends Named{
readonly name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name: string;
age = 30;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log(phrase + " " + this.name);
}
}
let user1: Greetable;
user1 = new Person("Thiago");
user1.greet("Hi there - I am");
console.log(user1);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Interfaces as Function Types
As we have already seen. We can define a type and let a function have that type, like this:
type AddFn = (a: number, b: number) => number;
let add: AddFn;
add = (n1:number, n2:number) => {
return n1 + n2;
};
2
3
4
5
6
7
The interface would be:
interface AddFn {
(a:number, b:number): number;
}
let add: AddFn;
add = (n1:number, n2:number) => {
return n1 + n2;
};
2
3
4
5
6
7
8
9
# Optional Parameters & Properties
optional properties on interfaces doesn't need to be implemented.
interface Named {
readonly name: string;
outputName?: string;
}
2
3
4
We can also add ?
to functions and parameters.
interface AddFn {
(a:number, b:number): number;
}
let add: AddFn;
add = (n1:number, n2:number) => {
return n1 + n2;
};
interface Named {
readonly name?: string;
outputName?: string;
}
interface Greetable extends Named{
readonly name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name?: string;
age = 30;
constructor(n?: string) {
if (n) {
this.name = n;
}
}
greet(phrase: string) {
if (this.name) {
console.log(phrase + " " + this.name);
} else {
console.log("Hi")
}
}
}
let user1: Greetable;
user1 = new Person();
user1.greet("Hi there - I am");
console.log(user1);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
output:
Hi
Person {age: 30}
2
TIP
There is no translation for interfaces, JavaScript doesn't have this feature.
# Advanced Types
# Intersection Types
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
}
type ElevatedEmployee = Admin & Employee;
const e1: ElevatedEmployee = {
name: 'Thiago',
privileges: ['create-server'],
startDate: new Date()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
We could have achieved the same with interfaces:
interface Admin {
name: string;
privileges: string[];
};
interface Employee {
name: string;
startDate: Date;
}
interface ElevatedEmployee extends Employee, Admin {};
const e1: ElevatedEmployee = {
name: 'Thiago',
privileges: ['create-server'],
startDate: new Date()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Intersection types can be used with any type.
type Combinable = string | number;
type Numeric = number | boolean;
type Universal = Combinable & Numeric;
2
3
# More on Type Guards
Sometimes we need to use type guards to use combinable types:
This example uses typeof
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
2
3
Other ways to use type guards are:
in this case we cannot check using typeof Employee
because Employee is of object type.
type UnknownEmployee = Employee | Admin;
function printEmployeeInformation(emp: UnknownEmployee) {
console.log('Name: ' + emp.name);
if ('privileges' in emp){
console.log('Privileges: ' + emp.privileges);
}
if ('startDate' in emp) {
console.log('Privileges: ' + emp.startDate);
}
}
printEmployeeInformation(e1);
printEmployeeInformation({name: 'Thiago', startDate: new Date()});
2
3
4
5
6
7
8
9
10
11
12
13
14
output:
Name: Thiago
Privileges: create-server
Privileges: Wed Apr 13 2022 15:41:28 GMT+1200 (New Zealand Standard Time)
Name: Thiago
Privileges: Wed Apr 13 2022 15:41:28 GMT+1200 (New Zealand Standard Time)
2
3
4
5
When working with classes we can also use the instanceof
type guard.
In this problem we can type guard like this: (with the if)
class Car {
drive() {
console.log('Driving ...');
}
}
class Truck {
drive() {
console.log('Driving ...');
}
loadCargo(amount: number) {
console.log('Loading cargo...' + amount);
}
}
type Vehicle = Car | Truck;
const v1 = new Car();
const v2 = new Truck();
function useVehicle(vehicle: Vehicle) {
vehicle.drive();
if ('loadCargo' in vehicle) {
vehicle.loadCargo(1000);
}
}
useVehicle(v1);
useVehicle(v2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
of like this:
class Car {
drive() {
console.log('Driving ...');
}
}
class Truck {
drive() {
console.log('Driving ...');
}
loadCargo(amount: number) {
console.log('Loading cargo...' + amount);
}
}
type Vehicle = Car | Truck;
const v1 = new Car();
const v2 = new Truck();
function useVehicle(vehicle: Vehicle) {
vehicle.drive();
if (vehicle instanceof Truck) {
vehicle.loadCargo(1000);
}
}
useVehicle(v1);
useVehicle(v2);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Discriminated Unions
A special type of type guard is the discriminated union. It makes implement type guard easier.
We can create a discriminated union by giving every interface which should be part of the union an extra property.
In this case lets use type
bird and horse.
interface Bird {
type: 'bird';
flyingSpeed: number;
}
interface Horse {
type: 'horse';
runningSpeed: number;
}
type Animal = Bird | Horse;
function moveAnimal(animal: Animal) {
let speed;
switch (animal.type) {
case "bird":
speed = animal.flyingSpeed;
break;
case "horse":
speed = animal.runningSpeed;
}
console.log('Moving with speed: ' + speed);
}
moveAnimal({type: 'bird', flyingSpeed: 10});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
output:
Moving with speed: 10
This is a discriminated union because we have one common property in every object that makes up the union which describes that object.
# Type Casting
It tells TypeScript that some value is of a specific type when TypeScript cannot tell on his own.
TypeScript can detect a <p>
tag on the html.
const paragraph = document.querySelector("p");
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Understand Typescript</title>
<script src="../dist/app.js" defer></script>
</head>
<body>
<p></p>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
But if we set as userInputElement
we got an error:
const userInputElement = document.getElementById('message-output')!;
userInputElement.value = "Hello"; // TypeScript error
2
3
We can type cast with<HTMLInputElement>
in front of the element, because we have the dom
library in the tsconfig
file.
const userInputElement =<HTMLInputElement>document.getElementById('message-output')!;
userInputElement.value = "Hello"; // now this works
2
3
We can also typecast with the as
keyword.
const userInputElement = document.getElementById('message-output')! as HTMLInputElement;
userInputElement.value = "Hello";
2
3
TIP
The !
after the element tells TypeScript that the expression in front of it will never yield null.
Like in here: document.getElementById('message-output')!
the alternative to this is:
if (userInputElement) {
userInputElement.value = "Hello";
}
2
3
or using Typecasting:
const userInputElement = document.getElementById('message-output');
if (userInputElement) {
(userInputElement as HTMLInputElement).value = "Hello";
}
2
3
4
5
# Index Properties
[prop: string]: string;
We are saying here the property name will be a string and also the content will be a string.
interface ErrorContainer {
[prop: string]: string;
}
const errorBag: ErrorContainer = {
email: "Not a valid email!",
username: "Must start with capital letter"
}
2
3
4
5
6
7
8
# Function Overloads
This allows us to define multiple function signatures. Multiple possible ways to call a function with different parameters.
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
const result = add("Thiago", " Souto");
result.split(" ");
2
3
4
5
6
7
8
9
10
11
12
13
14
With this we can use .split
which is a string method. Before TypeScript wouldn't know that result
would be a string.
# Optional Chaining
let:
const fetchUserData = {
id: "u1",
name: "Thiago",
job: {title: "CHO", description: "My own company"}
};
console.log(fetchUserData.job.title);
2
3
4
5
6
7
If we don't know if we will have the job
property, like in an API request for instance.
In JavaScript we can try to access job
and then if it works we access title
like this:
const fetchUserData = {
id: "u1",
name: "Thiago",
job: {title: "CHO", description: "My own company"}
};
console.log(fetchUserData.job && fetchUserData.job.title);
2
3
4
5
6
7
With TypeScript we have a nicer way, using a ?
.
console.log(fetchUserData.job?.title);
or
console.log(fetchUserData?.job?.title);
# Nullish Coalescing
Used when you don't know if the object is null or undefined.
const userInput = null;
const storeData = userInput ?? "DEFAULT";
console.log(storeData);
2
3
4
5
output:
DEFAULT
const userInput = " ";
const storeData = userInput ?? "DEFAULT";
console.log(storeData);
2
3
4
5
The output will be an empty string, because it's not undefined or null.
# Generics
# Built-in Generics & What are Generics
const names: Array<string> = ["Thiago", "Manuel"];
names[0].split( " ");
2
With this we can use .split
which is a string method.
We can use with Promises
as well.
const promise: Promise<string> = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("This is done");
}, 2000);
});
promise.then(data => {
data.split(" ");
})
2
3
4
5
6
7
8
9
# Creating a Generic Function
With generic types we are telling TypeScript that these 2 parameters nac and often will be different types, and therefore TypeScript is able to understand that we are not just working with some random object type. And then we can use .age
.
function merge<T, U>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
const mergedObj = merge({name: "Thiago", hobbies: ["Sports"]}, {age: 38});
console.log(mergedObj);
console.log(mergedObj.age);
2
3
4
5
6
7
We can also specify the types. But this is redundant since TypeScript inferred the types.
function merge<T, U>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
const mergedObj = merge<{name: string, hobbies: string[]}, {age: number}>({name: "Thiago", hobbies:["Sports"]}, {age: 38});
console.log(mergedObj);
console.log(mergedObj.age);
2
3
4
5
6
7
# Working with Constraints
In the previous example, we allowed the parameters to be of any type, now we will set such as there has to be an object, by restricting the type.
function merge<T extends object, U extends object>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
const mergedObj = merge<{name: string, hobbies: string[]}, {age: number}>({name: "Thiago", hobbies:["Sports"]}, {age: 38});
console.log(mergedObj.age);
2
3
4
5
6
In this case, we would have an error if we passed 38 as age, since 38 is a number and not an object.
function merge<T extends object, U extends object>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
const mergedObj = merge<{name: string, hobbies: string[]}, {age: number}>({name: "Thiago", hobbies:["Sports"]}, 38); // error
console.log(mergedObj.age);
2
3
4
5
6
7
# Another Generic Function
We get now an interface with a length property.
interface Lengthy {
length: number;
}
function countAndDescribe<T extends Lengthy>(element: T) {
let descriptionText = "Got no value. ";
if (element.length === 1) {
descriptionText = "Got 1 element. ";
} else if (element.length > 1 ) {
descriptionText = "Got " + element.length + " elements";
}
return [element, descriptionText];
}
console.log(countAndDescribe("Hello There!!"));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# The keyof
Constraint
We use keyof
to tell TypeScript that we want to ensure that we have the correct structure. This will guarantee that we do not access a property that does not exist.
function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U) {
return "Value: " + obj[key];
}
extractAndConvert({name: "Thiago"}, "name");
2
3
4
5
# Generic Classes
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data.splice(this.data.indexOf(item), 1);
}
getItems() {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("Thiago");
textStorage.addItem("Max");
textStorage.removeItem("Thiago");
console.log(textStorage.getItems());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
output:
Array(1)
0: "Max"
length: 1
[[Prototype]]: Array(0)
2
3
4
TIP
removeItem(item: T) {
if(this.data.indexOf(item) === -1 ){
return
}
this.data.splice(this.data.indexOf(item), 1);
}
2
3
4
5
6
In this case we need to check to see if the item exists, otherwise we could delete the wrong item.
But the ideal would be to specify that the function only works with primitive types and not objects.
like this:
class DataStorage<T extends string | number | boolean> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
if(this.data.indexOf(item) === -1 ){
return
}
this.data.splice(this.data.indexOf(item), 1);
}
getItems() {
return [...this.data];
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Generic Utility Types
# Partial
Types
interface CourseGoal {
title: string;
description: string;
completionDate: Date;
}
function createCourseGoal(
title: string,
description: string,
date: Date
): CourseGoal {
let courseGoal: Partial<CourseGoal> = {};
courseGoal.title = title;
courseGoal.description = description;
courseGoal.completionDate = date;
return courseGoal as CourseGoal;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Readonly
Types
const names: Readonly<string[]> = ["Max", "Thiago"];
# Generic Types vs Union Types
TIP
You want union types when you are flexible to have a different type with every method called, or every function call.
Generic Types lock in the type to be used throughout