- TypeScript
- Decorators
- Drag and Drop Project
- Importing/Exporting - Split Project
- Webpack With TypeScript
- Third Party Libraries and TypeScript
- React App + TypeScript
- Node + Express with TypeScript
-
Install TypeScript package globally with the command
npm install typescript -g
-
To manually generate a JavaScript file, use the command
tsc <file_name.ts>
-
Enable watch mode to automatically update our JavaScript file, once we save our TypeScript file. For That just add
--w
or--watch
tsc <file_name.ts> --w # or tsc <file_name.ts> --watch
-
To watch all .ts of our project, we need to run the following command just once in our project and TypeScript will take care of all .ts files for us
tsc --init
- Once we ran the command, TypeScript will create a new file
tsconfig.json
- This indicates to TypeScript which this file (
tsconfig.json
) lives and all sub-folders should be managed by TypeScript - In
tsconfig.json
we can enable extra configuration for our project - Another thing, we can also exclude certain files from compilation
- By adding in the end of the file
"exclude"
,"exclude"
is an array where we can define the path of the files that we want to exclude
- By adding in the end of the file
{ "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ // "outDir": "./", /* Redirect output structure to the directory. */ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ // "noEmit": true, /* Do not emit outputs. */ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ /* Additional Checks */ // "noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "exclude": [ "analytics.ts" ] }
-
We can also use wildcards to exclude certain type of files
"exclude": [ "*.dev.ts" ]
-
Or we can exclude from any folder that matches the criteria
"exclude": [ "**/*.dev.ts" ]
- Once we ran the command, TypeScript will create a new file
-
One thing that we might do for every project is to exclude the node_modules, because we don't want to compile the
.ts
files from our libraries- By default, TypeScript already excludes the node_modules
"exclude": [ "**/*.dev.ts", "node_modules" ]
-
Another thing that we can also do is to manually include the files.
-
If we specify the
"include"
, we have manually set all the files that we want to TypeScript to compile"include": [ "app.ts" ]
-
We can also set extra configuration
- In the configuration below, we need to manually add a new field (noEmitOnError), by default is set to false.
- If set to true, TypeScript won't compile the .ts file if there is any error
- sourceMap, it a good option to debug our
.ts
using Chrome Dev Tools
{ "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ "lib": [ "DOM", "ES6", "DOM.Iterable", "ScriptHost" ], /* Specify library files to be included in the compilation. */ "sourceMap": true, /* Generates corresponding '.map' file. */ "outDir": "./dist", /* Redirect output structure to the directory. */ "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "removeComments": true, /* Do not emit comments to output. */ "noEmitOnError": true, /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ /* Additional Checks */ "noUnusedLocals": true, /* Report errors on unused locals. */ "noUnusedParameters": false, /* Report errors on unused parameters. */ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ /* Module Resolution Options */ "esModuleInterop": true, /* Enables emit interoperability between /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "exclude": [ "analytics.ts" ] }
- In the configuration below, we need to manually add a new field (noEmitOnError), by default is set to false.
-
Then, after we've created the
tsconfig.json
- we execute
tsc --w
(--w
, watch mode) - And this command will search for all
.ts
in our project and compile the.js
version of each file
- we execute
-
The easiest way to handle error from non existing elements in our DOM it to add and
if
statement to check if the element is truthyconst button1 = document.querySelector('button'); if (button1) { button1.addEventListener('click', () => { console.log('Clicked!'); }); }
-
Another option is to add a
?
right after the elementconst button2 = document.querySelector('button'); button2?.addEventListener('click', () => { console.log('Clicked!'); });
-
Normal function returning the result to be used in our code
function add(a: number, b: number) { return a + b; } console.log(add(3, 5)); // 8
-
Arrow function with default value, returning the result to be used in our code
const addDefault = (a: number, b: number = 1) => a + b; console.log(addDefault(1)); // 2
const birthYear = (age: number) => { return 2020 - age; }; const year = birthYear(33); printOutput(year); // 1987
-
Arrow function receiving a number or a string, not using the result in our program
- If we are not returning anything from a function, it's a good practice to define as void
- so if we are using an arrow function we fist have to define the input type that our function can receive
printOutput: (a: number | string)
- Then we have to explicit indicate that this function is not returning anything
=> void
- And the rest is just a normal arrow function
- so if we are using an arrow function we fist have to define the input type that our function can receive
const printOutput: (a: number | string) => void = (output) => { console.log(output); };
- If we are not returning anything from a function, it's a good practice to define as void
-
We can also create a function that accepts
n
types of argumentsconst concatStr = (a: string, b: string) => a + b; printOutput(addDefault(3, 8)); printOutput(concatStr('Roger', 'Takeshita')); // 11 // RogerTakeshita
const numbers: number[] = [];
const test: number[] = [1, 2, 3, 4, 5, 6];
numbers.push(...test);
console.log(numbers);
// [1, 2, 3, 4, 5, 6]
-
Destructuring an object and assigning a different name from the object
const person = { firstName: 'Roger', lastName: 'Takeshita', age: 33, }; const { firstName: userName, age } = person; console.log(userName, age); // Roger 33
-
We can also destructuring an array by position, and the rest of remaining values we could assign to another variable
const sports: string[] = ['Hiking', 'Cycling', 'Baseball', 'Basketball']; const [sport1, sport2, ...rest] = sports; console.log(sport1); console.log(sport2); console.log(rest); // Hiking // Cycling // ["Baseball", "Basketball"]
-
Properties
- public properties, where can be accessed anywhere outside of the class
- private properties, where can be accessed only inside the class, but not from subclasses - inheritance
- protected properties, where cannot be accessed outside of the class, but can be accessed from their subclass - inheritance
-
Constructor
- The constructor immediately instantiate the properties defined inside the constructor, when we invoke a new instance of the class
- With TypeScript we can create a new property, assign in one line:
public
,private
,protected
readonly
- read mode only, cannot be modified- assign the
type
-
Methods
- With a method of a class, we can create custom functions for our classes
- But if we need access to certain properties from the constructor to a subclass, we need to assign to the type of class, in other words assign name of the class
class Department { // public publicProperty: string; // private privateProperty: string; protected employees: string[] = []; constructor(private readonly id: string, public name: string) {} describe(this: Department) { console.log(`Department: ${this.name} (${this.id})`); } addEmployee(employee: string) { this.employees.push(employee); } printEmployeeInformation() { console.log(this.employees.length); console.log(this.employees); } }
-
We can inherit properties, methods and override methods from the parent class
- In a derived class, the super keyword represents the parent superclass and must be called before the this keyword can be used in the constructor.
const it = new ITDepartment('d3', ['Roger']); it.addEmployee('Mike'); it.addEmployee('Joy'); it.addEmployee('Yumi'); it.name = 'New IT'; it.describe(); it.printEmployeeInformation(); console.log(it); class AccountingDepartment extends Department { constructor(id: string, private reports: string[]) { super(id, 'Accounting Reports'); } addEmployee(name: string) { if (name === 'Bob') return; this.employees.push(name); } addReport(text: string) { this.reports.push(text); } printReports() { console.log(this.reports); } } const accDepartment = new AccountingDepartment('d4', []); accDepartment.name = 'New Accounting Department'; accDepartment.describe(); accDepartment.addEmployee('Bob'); accDepartment.addEmployee('Marley'); accDepartment.addReport('Report 1'); accDepartment.addReport('Report 2'); accDepartment.addReport('Report 3'); accDepartment.printReports(); accDepartment.printEmployeeInformation(); console.log(accDepartment);
-
Encapsulating more complex logic to our class, we can use
get
(getter) andset
(setter) to define a new method that we can access like a property of the classclass 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 a valid value'); this.addReport(value); } constructor(id: string, private reports: string[]) { super(id, 'Accounting Reports'); this.lastReport = reports[0]; } addEmployee(name: string) { if (name === 'Bob') return; this.employees.push(name); } addReport(text: string) { this.reports.push(text); this.lastReport = text; } printReports() { console.log(this.reports); } } const accDepartment = new AccountingDepartment('d4', []); // console.log(accDepartment.mostRecentReport); accDepartment.mostRecentReport = 'Report using setter'; accDepartment.name = 'New Accounting Department'; accDepartment.describe(); accDepartment.addEmployee('Bob'); accDepartment.addEmployee('Marley'); accDepartment.addReport('Report 1'); accDepartment.addReport('Report 2'); accDepartment.addReport('Report 3'); accDepartment.printReports(); accDepartment.printEmployeeInformation(); console.log(accDepartment); console.log(accDepartment.mostRecentReport);
-
Call a method without instantiating a class
-
For that we have to define the method / property as static
-
ATTENTION: With static methods / properties in our class, we cannot access invoke inside other methods in our Class directly. This static method / property is only available outside of the class
- static methods/properties are detached from the class, that's why wen can't access using
this
keyword - To access the static method/property inside of a class method, we have to call the class itself to access the method/property
Department.fiscalYear
class Department { static fiscalYear: number = 2020; // public publicProperty: string; // private privateProperty: string; protected employees: string[] = []; constructor(private readonly id: string, public name: string) {} static createEmployee(name: string) { return { name, }; } describe(this: Department) { console.log(`Department: ${this.name} (${this.id})`); } addEmployee(employee: string) { this.employees.push(employee); } printEmployeeInformation() { console.log(this.employees.length); console.log(this.employees); } } const newEmployee = Department.createEmployee('John'); console.log(newEmployee, Department.fiscalYear); const accounting = new Department('d1', 'Accounting'); accounting.addEmployee('Roger'); accounting.addEmployee('Thaisa'); accounting.name = 'New Accounting';
- static methods/properties are detached from the class, that's why wen can't access using
-
Abstract classes, are classes that we don't need to define complete structure of a method, but we want to enforce that our subclasses also have the same method but with different implementation
-
This way we only define the method that we want to enforce as
abstract
and also we need to define our class asabstract
- for methods, we we are not returning any value, we should assign
void
- Then all of subclasses will inherit this method, and we will need to create the method to that subclass, otherwise, we'll get an error
abstract class Department { static fiscalYear: number = 2020; // public publicProperty: string; // private privateProperty: string; protected employees: string[] = []; constructor(protected readonly id: string, public name: string) {} static createEmployee(name: string) { return { name, }; } // describe(this: Department) { // console.log(`Department: ${this.name} (${this.id})`); // } 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}`); } }
- for methods, we we are not returning any value, we should assign
-
Single instance of an object
-
To create a private constructor, we just need to assign
private
in front of the constructor- But with that, we no longer can create a new instance of class (
new AccountingDepartment('d4', [])
) - To have access to the private constructor we have to create a
static
method, this way we don't need to invoke the class, but just the method
- But with that, we no longer can create a new instance of class (
-
Then we need to create a
private static
instance, type class, so we can check if there is already an existing class, if yes, we use that one, otherwise, create oneclass 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 a valid value'); this.addReport(value); } private constructor(id: string, private reports: string[]) { super(id, 'Accounting Reports'); this.lastReport = reports[0]; } static getInstance() { if (this.instance) { return this.instance; } this.instance = new AccountingDepartment('d4', []); return this.instance; } describe() { console.log(`Custom Accounting Department - ID: ${this.id}`); } addEmployee(name: string) { if (name === 'Bob') return; this.employees.push(name); } addReport(text: string) { this.reports.push(text); this.lastReport = text; } printReports() { console.log(this.reports); } } // const accDepartment = new AccountingDepartment('d4', []); const accDepartment = AccountingDepartment.getInstance(); const accDepartment2 = AccountingDepartment.getInstance(); console.log(accDepartment, accDepartment2); // console.log(accDepartment.mostRecentReport); accDepartment.mostRecentReport = 'Report using setter'; accDepartment.name = 'New Accounting Department'; accDepartment.describe(); accDepartment.addEmployee('Bob'); accDepartment.addEmployee('Marley'); accDepartment.addReport('Report 1'); accDepartment.addReport('Report 2'); accDepartment.addReport('Report 3'); accDepartment.printReports(); accDepartment.printEmployeeInformation(); console.log(accDepartment); console.log(accDepartment.mostRecentReport);
-
One option of an interface would be a
type
object -
We often can use interchangeably
interface
ortype
type Person = { name: string; age: number; greet(phrase: string): void; } let user1: Person; user1 = { name: 'Roger', age: 33, greet(phrase: string) { console.log(`${phrase} ${this.name}`); } } user1.greet('Hi there - I am');
-
The difference between an interface and type:
- With interface we can only use to describe the structure of an object. While a
type
, it can be used to store other things likeunion types
(multiple types into one type)let text: string | string[];
- When we define as an interface, it's clear that we want to define only the structure of the object, while type is not always true
- An interface can be implemented inside a class
- To do so, we have to
implements
, similar toextends
but we can assign multiple interfaces - Then we just need to create the method inside our class, and this method will follow the structure of our interface
- To do so, we have to
- Interfaces are often used to share functionalities among different classes, not concrete about the implementation but regarded to structure / features that a class should have
- Similar to an
abstract class
- With interface we can only use to describe the structure of an object. While a
-
Not allowed in an interface
public
private
protected
-
Allowed
readonly
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('Roger'); user1.name = 'Not Allowed'; user1.greet('Hi there - I am'); console.log(user1);
-
We can combine interfaces with the help of
extends
(just like in a class), the the sub interface will inherit everything from the parent interface- the only difference is that interfaces can extend more than one parent interface (with classes that's not allowed)
interface Named { readonly name: string; } interface Greetable extends Named { 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('Roger'); // user1.name = 'Not Allowed'; <--- will get an error user1.greet('Hi there - I am'); console.log(user1);
-
Most common way to create a function structure would be using
type
type AddFn = (a: number, b: number) => number;
-
An alternative would be to create using function interface, for that we need to an anonymous function
// type AddFn = (a: number, b: number) => number; interface AddFn { (a: number, b: number): number; } let add1: AddFn; add1 = (n1: number, n2: number) => { return n1 + n2; };
-
Not always we want to enforce the structure of the interface, fot that we can create optional properties by adding a
?
(question mark) after the name of the property- We could also mark methods as optional
myMethod?(){...}
interface Named { readonly name?: string; outputName?: string; } interface Greetable extends Named { 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.name = 'Not Allowed'; user1.greet('Hi there - I am'); console.log(user1);
- We could also mark methods as optional
-
Intersection types (&)
- Combine one or more
types
type Admin = { name: string; privileges: string[]; }; type Employee = { name: string; startDate: Date; }; type ElevatedEmployee = Admin & Employee; type Combinable = string | number; type Numeric = number | boolean; type Universal = Combinable & Numeric; //+ New object type ElevatedEmployee const e1: ElevatedEmployee = { name: 'Roger', privileges: ['create-server'], startDate: new Date(), };
- Combine one or more
type Admin = {
name: string;
privileges: string[];
};
type Employee = {
name: string;
startDate: Date;
};
type ElevatedEmployee = Admin & Employee;
type Combinable = string | number;
type Numeric = number | boolean;
type Universal = Combinable & Numeric;
function add2(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
-
This is called a Type Guard
- Type Guard is just a term that describes the idea or approach of checking if a certain property or method before using it.
- It allows us to the flexibility that
union type
gives us and still assure that our code run correctly at run time
if (typeof a === 'string' || typeof b === 'string') { return a.toString() + b.toString(); }
-
Checking if a property exist
class Car { drive() { console.log('Driving...'); } } class Truck { drive() { console.log('Driving truck...'); } 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);
-
Elegant way to check if a property exists
- We can use instanceof (it's vanilla JS)
- The
instanceof
operator tests whether theprototype
property of a constructor appears anywhere in the prototype chain of an object
function Car(make, model, year) { this.make = make; this.model = model; this.year = year; } const auto = new Car('Honda', 'Accord', 1998); console.log(auto instanceof Car); // expected output: true console.log(auto instanceof Object); // expected output: true
-
For objects we can use
in
orinstanceof
-
And for other cases we can use
typeof
-
Discriminated Union
is a pattern which we can use to work with union types that makes implementing type guards easier -
It's available when we are working with object types
-
The discriminant is a singleton type property which is common in each of the elements of the union (tag).
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; break; } console.log(`Moving at speed: ${speed}`); } moveAnimal({ type: 'bird', flyingSpeed: 10 }); moveAnimal({ type: 'horse', runningSpeed: 30 });
-
Type Casting helps you tell TypeScript that some value is of a specific type
- Option 1 - using
<...>
before the element - Option 2 define
as
the element type after targeting the element
// const userInputEl = <HTMLInputElement>document.getElementById('user-input')!; const userInputEl = document.getElementById('user-input')! as HTMLInputElement; userInputEl.value = 'Hi There!';
!
in the end of the element tells TypeScript that the expression in front of it will never yieldnull
- Option 1 - using
-
Works with object, we could define an error container where we define all the possible errors, and using generic key/value pairs to access the information
[key: string]: string
- Where the
key
is of typestring
- and the
value
is of typestring
interface ErrorContainer { [key: string]: string; } const errorBag: ErrorContainer = { email: 'Not a valid email!', username: 'Must start with a capital character!', };
-
Function overloads is a feature that allows us define multiple function signatures
- Multiple ways to call the function with multiple parameters to do something inside of that function
function add2(a: number, b: number): number; function add2(a: string, b: string): string; function add2(a: number, b: string): string; function add2(a: string, b: number): string; function add2(a: Combinable, b: Combinable) { if (typeof a === 'string' || typeof b === 'string') { return a.toString() + b.toString(); } return a + b; } const result = add2('Roger', ' Takeshita'); console.log(result.split(' ')); const result1 = add2(1, 3); console.log(result1);
-
by adding a
?
after the object that we are unsure that exists or not. If the property exist then it will accesses the next property, and so on...const fetchUserData = { id: 'ui', name: 'Max', job: { title: 'CEO', description: 'My own company', }, }; // console.log(fetchUserData.job && fetchUserData.job.title); console.log(fetchUserData?.job?.title);
-
the double
?
checks if the value is reallynull
orundefined
different from normal JS that an empty string isfalsy
const userInput = ''; const storedData = userInput || 'DEFAULT'; console.log(storedData); // DEFAULT const userInput2 = ''; const storedData2 = userInput2 ?? 'DEFAULT'; console.log(storedData2); //
-
Generic type
Array<type_here>
//! Generic Type Array of Strings // const names = ['Roger', 'Thaisa']; const names: Array<string> = []; // equal to string[] // names[0].split(' '); //! Generic Type Promise - Returning a String const promise: Promise<string> = new Promise((resolve, reject) => { setTimeout(() => { resolve('This is done'); }, 2000); }); promise.then((data) => { console.log(data.split(' ')); }); //! Generic Type Promise - Returning a Number const promise2: Promise<number> = new Promise((resolve, reject) => { setTimeout(() => { resolve(10.6); }, 2000); }); promise2.then((data) => { console.log(Math.ceil(data)); });
function merge(objA: object, objB: object) {
return Object.assign(objA, objB);
}
console.log(merge({ name: 'Roger' }, { age: 33 }));
const mergedObj = merge({ name: 'Roger' }, { age: 33 });
console.log(mergedObj);
// {name: "Roger", age: 33}
// console.log(mergedObj.name); // this won't work, because TypeScript doesn't know this
//+ One alternative is to use type casting
const mergedObjAlternative1 = merge({ name: 'Roger' }, { age: 33 }) as {
name: string;
age: number;
};
console.log(mergedObjAlternative1.name);
//+ A better approach is to use generics to user generic objects
function merge2<T, U>(objA: T, objB: U) {
return Object.assign(objA, objB);
}
const mergedObjAlternative2 = merge2({ name: 'Roger' }, { age: 33 });
console.log(mergedObjAlternative2.name);
-
The following code JavaScript fails silently, JavaScript won't throw an error, and our object doesn't have a property
33
-
Currently we are saying that
T
andU
should be any typefunction merge3<T, U>(objA: T, objB: U) { return Object.assign(objA, objB); } const mergedObjAlternative3 = merge3({ name: 'Roger' }, 33); console.log(mergedObjAlternative3.age); // TypeScript will throw an error
-
Generic Type Constraints
- We add extends after the object that we want to constraints
- We can set any type of constraints, custom type, union types...
function merge3<T extends object, U extends object>(objA: T, objB: U) { return Object.assign(objA, objB); } const mergedObjAlternative3 = merge3({ name: 'Roger' }, { age: 33 }); console.log(mergedObjAlternative3.name); console.log(mergedObjAlternative3);
-
we can create a custom
interface
and then extends our generic function, then explicit indicate that our function will return a tuple where the first position is oftype any
(not a number type), and the second position will be atype string
.interface Lengthy { length: number; } function countAndDescribe<T extends Lengthy>(element: T): [T, string] { let descriptionText = 'Got no Value'; if (element.length === 1) { descriptionText = 'Got 1 element'; } else if (element.length > 0) { descriptionText = `Got ${element.length} elements`; } return [element, descriptionText]; } console.log(countAndDescribe('Hi there!')); console.log(countAndDescribe(['Sports', 'Cooking'])); console.log(countAndDescribe([]));
-
We can use the
keyof
constraint to extend certain types to the class to be more specific (instead oftype any
)class DataStorage<T extends string | number | boolean> { 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('Roger'); textStorage.addItem('Thaisa'); textStorage.removeItem('Roger'); console.log(textStorage.getItems()); const numberStorage = new DataStorage<number>(); numberStorage.addItem(1); numberStorage.addItem(2); numberStorage.addItem(3); console.log(numberStorage.getItems());
-
To work with object, it's not that simple, because with objects, the only way to remove an object, it's by accessing the pointer of that object
-
Just because the structure of an object might be the same, this doesn't mean that the pointer in memory are the same, that's why we can't simply removeItem({name: 'Roger'})
-
One work around is to define the object as a constant, and then when we want to delete this object, we reference the same constant.
-
Beside that, we can constraint our class to only extends to
stings
,numbers
andbooleans
const objStorage = new DataStorage<object>(); const rogerObj = { name: 'Roger' }; objStorage.addItem(rogerObj); objStorage.addItem({ name: 'Thaisa' }); objStorage.removeItem(rogerObj); console.log(objStorage.getItems());
-
Partials
-
Partial is another type of property where it assigns everything as optional, this way when we have an empty array but we want to assign an interface so we can use this object later.
-
We can assign as Partial with the type
<CourseGoal>
-
And then, before we return the new object, we convert it as a CourseGoal object, because so far we have a as a Partial object and not as CourseGoal object
interface CourseGoal { title: string; description: string; completeUntil: Date; } function createCourseGoal(title: string, description: string, date: Date) { let courseGoal: Partial<CourseGoal> = {}; courseGoal.title = title; courseGoal.description = description; courseGoal.completeUntil = date; return courseGoal as CourseGoal; }
-
-
Readonly
const names2: Readonly<string[]> = ['Roger', 'Thaisa']; names2.push('Yumi'); // <---- It will throw an error names2.pop(); // <---- It will throw an error
-
Create new folder
mkdir 2_Decorators
cd 2_Decorators
npm init
npm i
tsc --init
-
Then we have to config our
tsconfig.json
- We first need to change the target from
es5
toes6
- Then we have to enable the decorators, otherwise, we won't be able to user decorators in our project
{ "compilerOptions": { /* Basic Options */ "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "sourceMap": true /* Generates corresponding '.map' file. */, "outDir": "./dist" /* Redirect output structure to the directory. */, "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, "removeComments": true /* Do not emit comments to output. */, "noEmitOnError": true, /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, /* Module Resolution Options */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, /* Experimental Options */ "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } }
- We first need to change the target from
-
Packages
- Install
npm i lite-server
- Install
-
in
package.json
- Config our
start
script aslite-server
{ "name": "2_decorators", "version": "1.0.0", "description": "TypeScript Decorators", "main": "app.js", "scripts": { "start": "lite-server", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Roger Takeshita", "license": "ISC", "dependencies": { "lite-server": "^2.5.4" } }
- Config our
-
Decorator is in the end just a function, a function that you apply to something
-
So we can declare as a normal function, the only difference is that the name of the function starts with a capital letter (by convention)
-
To use the decorator we have to add the
@
in front of the decorator- We declare before the of the thing that we want to apply this function/decorator
@
is a symbol recognized by TypeScript, after the@
we should point to the function/decorator (no execute()
)
-
Decorators receive argument(s) depending on where we want to apply the decorator
- For class we have one argument constructor
-
Decorators are executed when the class is defined, in other words, we don't even need to instantiate the class to execute the decorator
function Logger(constructor: Function) { console.log('Logging...'); console.log(constructor); } @Logger class Person { name = 'Roger'; constructor() { console.log('Creating person object...'); } } const person = new Person(); console.log(person);
-
The difference between a normal decorator and a decorator factories, is that we have to define the decorator returning an anonymous function.
- The advantage of doing that is that we can pass it arguments to the decorator that can be used inside of the the constructor
function Logger(logString: string) { return function (constructor: Function) { console.log(logString); console.log(constructor); }; } @Logger('LOGGING - PERSON') class Person { name = 'Roger'; constructor() { console.log('Creating person object...'); } } const person = new Person(); console.log(person);
-
Another example of decorator factories
- If we're not going to use the constructor, we have to indicates to TypeScript that we know that the the decorator factory needs an argument (a constructor in this case) but we are not going to use it. So we have to specify using
_
function WithTemplate(template: string, hookId: string) { return function (_: Function) { const hookEl = document.getElementById(hookId); if (hookEl) { hookEl.innerHTML = template; } }; } @WithTemplate('<h1>My Person Object</h1>', 'app') class Person { name = 'Roger'; constructor() { console.log('Creating person object...'); } }
- If we're not going to use the constructor, we have to indicates to TypeScript that we know that the the decorator factory needs an argument (a constructor in this case) but we are not going to use it. So we have to specify using
-
We could instantiate a new constructor of our class, then output the name to the DOM
function WithTemplate(template: string, hookId: string) { return function (constructor: any) { const hookEl = document.getElementById(hookId); const newPerson = new constructor(); if (hookEl) { hookEl.innerHTML = template; hookEl.querySelector('h1')!.innerHTML = newPerson.name; } }; } @WithTemplate('<h1>My Person Object</h1>', 'app') class Person { name = 'Roger'; constructor() { console.log('Creating person object...'); } }
-
We can use multiple decorators, but TypeScript executes bottom up
```TypeScript function Logger(logString: string) { return function (constructor: Function) { console.log(logString); console.log(constructor); }; } function WithTemplate(template: string, hookId: string) { return function (constructor: any) { console.log('Rendering template'); const hookEl = document.getElementById(hookId); const newPerson = new constructor(); if (hookEl) { hookEl.innerHTML = template; hookEl.querySelector('h1')!.innerHTML = newPerson.name; } }; } @Logger('LOGGING') @WithTemplate('<h1>My Person Object</h1>', 'app') class Person { name = 'Roger'; constructor() { console.log('Creating person object...'); } } ```
-
To add a decorator to a property, it's like adding to a class
-
The only differences are the arguments, properties the decorator has two arguments
- 1st - target
- if the target is a class, then it's the
constructor
- if the target is an object, then it's the
prototype
- if the target is a class, then it's the
- 2nd - property name
function Log(target: any, propertyName: string | symbol) { console.log('Property decorator!'); console.log(target, propertyName); } class Product { @Log title: string; private _price: number; set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error('Invalid Price - Should Be Positive!'); } } constructor(t: string, p: number) { this.title = t; this._price = p; } getPriceWithTax(tax: number) { return this._price * (1 + tax); } }
- 1st - target
-
Adding a decorator to an accessor (setter)
function Log(target: any, propertyName: string | symbol) { console.log('Property decorator!'); console.log(target, propertyName); } function Log2(target: any, name: string, descriptor: PropertyDescriptor) { console.log('Accessor decorator'); console.log(target); console.log(name); console.log(descriptor); } class Product { @Log title: string; private _price: number; @Log2 set price(val: number) { if (val > 0) { this._price = val; } else { throw new Error('Invalid Price - Should Be Positive!'); } } constructor(t: string, p: number) { this.title = t; this._price = p; } getPriceWithTax(tax: number) { return this._price * (1 + tax); } }
function Log3(
target: any,
name: string | symbol,
descriptor: PropertyDescriptor
) {
console.log('Method decorator');
console.log(target);
console.log(name);
console.log(descriptor);
}
class Product {
@Log
title: string;
private _price: number;
@Log2
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error('Invalid Price - Should Be Positive!');
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@Log3
getPriceWithTax(tax: number) {
return this._price * (1 + tax);
}
}
function Log4(target: any, name: string | symbol, position: number) {
console.log('Parameter decorator');
console.log(target);
console.log(name);
console.log(position);
}
class Product {
@Log
title: string;
private _price: number;
@Log2
set price(val: number) {
if (val > 0) {
this._price = val;
} else {
throw new Error('Invalid Price - Should Be Positive!');
}
}
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
@Log3
getPriceWithTax(@Log4 tax: number) {
return this._price * (1 + tax);
}
}
-
It's possible to have a
return value
inside a decorator (class and methods decorators) -
Working with class decorator
- With class decorators, we can return a new constructor function which will replace the old one, in other words will replace the class d
- So we could return a new class (anonymous - doesn't need to have a name), a new constructor function, and we could extends the new constructor
- In other words we are keeping all the original properties for the constructor and add new functionalities
- To do that, we have to call
super()
inside of our new constructor - just like a normal class to inherit from the parent class
- To do that, we have to call
function WithTemplate(template: string, hookId: string) { console.log('Template Factory'); return function <T extends { new (...args: any[]): { name: string } }>( originalConstructor: T ) { return class extends originalConstructor { constructor(..._: any[]) { super(); console.log('Rendering template'); const hookEl = document.getElementById(hookId); if (hookEl) { hookEl.innerHTML = template; hookEl.querySelector('h1')!.innerHTML = this.name; } } }; }; } @Logger('LOGGING') @WithTemplate('<h1>My Person Object</h1>', 'app') class Person { name = 'Roger'; constructor() { console.log('Creating person object...'); } } // const person = new Person(); // console.log(person);
-
We can also have a return value to other decorators, but not always the return value is respected
-
Decorators that we can return something:
methods
andaccessors
-
Decorators that TypeScript ignores the return value:
properties
andparameters
-
For
methods
andaccessors
we can execute another function/methods (descriptor
)- For
methods
the property descriptor we have:configurable
enumerable
value
(in our case it's a method)writable
- For
accessors
the property descriptor we have:configurable
enumerable
get
(getters)set
(setters)
- For
-
We could change the return value of the accessor and assign a new method to override the old method
- For example, we could have a new setter
- To do that we have to indicate to TypeScript that the we have a return value type
PropertyDescriptor
function Log2(target: any, name: string, descriptor: PropertyDescriptor): PropertyDescriptor { console.log('Accessor decorator'); console.log(target); console.log(name); console.log(descriptor); return {set ...} }
-
One way to bind
this
keyword to the method that is executed, and not the method that is calling, we need to bind the this, in the example bellow we are using arrow function to bind, but we could usebind(this)
(es5)class Printer { message = 'This works!'; showMessage() { console.log(this.message); } } const p = new Printer(); const button = document.querySelector('button')!; button.addEventListener('click', () => p.showMessage());
-
Elegant way to bind
this
using decoratorsfunction AutoBind(_: any, _2: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; const adjDescriptor: PropertyDescriptor = { configurable: true, enumerable: false, get() { const boundFn = originalMethod.bind(this); return boundFn; }, }; return adjDescriptor; } class Printer { message = 'This works!'; @AutoBind showMessage() { console.log(this.message); } } const p = new Printer(); const button = document.querySelector('button')!; button.addEventListener('click', p.showMessage);
interface ValidatorConfig {
[property: string]: {
[validatableProp: string]: string[]; // ['required', 'positive']
};
}
const registeredValidators: ValidatorConfig = {};
function Required(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: ['required'],
};
}
function PositiveNumber(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: ['positive'],
};
}
function validate(obj: any) {
const objValidatorConfig = registeredValidators[obj.constructor.name];
if (!objValidatorConfig) {
return true;
}
let isValid = true;
for (const prop in objValidatorConfig) {
for (const validator of objValidatorConfig[prop]) {
switch (validator) {
case 'required':
isValid = isValid && !!obj[prop];
break;
case 'positive':
isValid = isValid && obj[prop] > 0;
break;
}
}
}
return isValid;
}
class Course {
@Required
title: string;
@PositiveNumber
price: number;
constructor(t: string, p: number) {
this.title = t;
this.price = p;
}
}
const courseForm = document.querySelector('form')!;
courseForm.addEventListener('submit', (event) => {
event.preventDefault();
const titleEl = document.getElementById('title') as HTMLInputElement;
const priceEl = document.getElementById('price') as HTMLInputElement;
const title = titleEl.value;
const price = +priceEl.value;
const createdCourse = new Course(title, price);
if (!validate(createdCourse)) {
alert('Invalid input, please try again!');
return;
}
console.log(createdCourse);
});
-
We can create a class to interact with our
DOM
elements, to instantiate a HTML element. For example our form inindex.html
isn't visible<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <title>ProjectManager</title> <link rel="stylesheet" href="app.css" /> <script src="dist/app.js" defer></script> </head> <body> <template id="project-input"> <form> <div class="form-control"> <label for="title">Title</label> <input type="text" id="title" /> </div> <div class="form-control"> <label for="description">Description</label> <textarea id="description" rows="3"></textarea> </div> <div class="form-control"> <label for="people">People</label> <input type="number" id="people" step="1" min="0" max="10" /> </div> <button type="submit">ADD PROJECT</button> </form> </template> <template id="single-project"> <li></li> </template> <template id="project-list"> <section class="projects"> <header> <h2></h2> </header> <ul></ul> </section> </template> <div id="app"></div> </body> </html>
-
We can have access to the form template and render inside the
<div id="app">
, to do that, we have to first target ourtemplate
to have access to the form information -
We first create a class
- Define all the dom elements that we need to have access, and assign their respective
type
- In the constructor, then we create and connect the properties to the DOM
- Then after we immediately instantiate an object of this class, we want to render our form, and to do so, we can use
importNode()
from our DOM methods, and then we pass a pointer to our template content, and the second argument is the lvl (how deep we want to have access -true
to have access to the nested elements)
- Define all the dom elements that we need to have access, and assign their respective
class ProjectInput {
templateElement: HTMLTemplateElement;
hostElement: HTMLDivElement;
element: HTMLFormElement;
titleInputElement: HTMLInputElement;
descriptionInputElement: HTMLInputElement;
peopleInputElement: HTMLInputElement;
constructor() {
this.templateElement = document.getElementById(
'project-input'
)! as HTMLTemplateElement;
this.hostElement = document.getElementById('app')! as HTMLDivElement;
const importedNode = document.importNode(
this.templateElement.content,
true
);
this.element = importedNode.firstElementChild as HTMLFormElement;
this.element.id = 'user-input';
this.titleInputElement = this.element.querySelector(
'#title'
)! as HTMLInputElement;
this.descriptionInputElement = this.element.querySelector(
'#description'
)! as HTMLInputElement;
this.peopleInputElement = this.element.querySelector(
'#people'
)! as HTMLInputElement;
this.configure();
this.attach();
}
private submitHandler(event: Event) {
event.preventDefault();
console.log(this.titleInputElement.value);
}
// create an eventListener to to submit
private configure() {
this.element.addEventListener('submit', () => this.submitHandler);
}
// to have insert a new form to afterbegin of {}
private attach() {
this.hostElement.insertAdjacentElement('afterbegin', this.element);
}
}
const projectInput = new ProjectInput();
-
Notice that to our
submitHandler
to have access tothis.titleInputElement.value
we had to bind thethis
keyword, otherwise, thesubmitHandler
won't be able to access to the properties of the classprivate configure() { this.element.addEventListener('submit', () => this.submitHandler); }
-
Another elegant way to bind the
this
is using function/method decorators, then we could create a decorator to auto bind thethis
, using the function decorator properties available to TypeScript-
target
-
method name
-
descriptor (the property of this function)
-
We first create our
function/method decorator
- Where we save the original method
- and then we configure our descriptor
- and using the
getter
, to execute when we access the function, we can now override / set new properties to this method
- and using the
function AutoBind(_: any, _2: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; // store our original method const adjustedDescriptor: PropertyDescriptor = { configurable: true, get() { const boundFn = originalMethod.bind(this); return boundFn; }, }; return adjustedDescriptor; }
-
And then in our
submitHandler
andconfigure
method, we can now use the decorator to auto bind thethis
@AutoBind private submitHandler(event: Event) { event.preventDefault(); console.log(this.titleInputElement.value); } // create an eventListener to to submit private configure() { // this.element.addEventListener('submit', this.submitHandler.bind(this)); // this.element.addEventListener('submit', () => this.submitHandler); this.element.addEventListener('submit', this.submitHandler); }
-
-
We can create a new function (a private one) to validate the user input
- To do so, we could create a function that returns a
tuple
orvoid (undefined)
for functions. - And once the user
submit
the form, it will check if theuserInput
is an array (tuple) before doing anything else.
private gatherUserInput(): [string, string, number] | void { const enteredTitle = this.titleInputElement.value; const enteredDescription = this.titleInputElement.value; const enteredPeople = this.peopleInputElement.value; if ( enteredTitle.trim().length === 0 || enteredDescription.trim().length === 0 || enteredPeople.trim().length === 0 ) { alert('Invalid input, please try again'); return; } return [enteredTitle, enteredDescription, +enteredPeople]; } @AutoBind private submitHandler(event: Event) { event.preventDefault(); const userInput = this.gatherUserInput(); if (Array.isArray(userInput)) { const [title, description, people] = userInput; console.log(title, description, people); this.clearInputs(); } }
- To do so, we could create a function that returns a
- To build a storage like Redux, we can use the singleton pattern to create a single source of truth.
-
Create private variables with their respective type and initial value.
-
Single instance of an object
-
To create a private constructor, we just need to assign private in front of the constructor
-
But with that, we no longer can create a new instance of the class (
new className()
) -
To have access to the private constructor we have to create a static method, this way we don't need to invoke the class, but just the method
- ATTENTION: With static methods / properties in our class, we cannot be accessed directly inside other methods in our Class. This static method / property is only available outside of the class
- Static methods/properties are detached from the class, that's why wen can't access using this keyword
- To access the static method/property inside of a class method, we have to call the class itself to access the method/property
-
Then we need to create a private static instance, type class, so we can check if there is already an existing class, if yes, we use that one, otherwise, create one
class ProjectState { private listeners: any[] = []; private projects: any[] = []; private static instance: ProjectState; private constructor() {} static getInstance() { if (this.instance) { return this.instance; } this.instance = new ProjectState(); return this.instance; } addListener(listenerFn: Function) { this.listeners.push(listenerFn); } addProject(title: string, description: string, numOfPeople: number) { const newProject = { id: Math.random().toString(), title, description, people: numOfPeople, }; this.projects.push(newProject); for (const listenerFn of this.listeners) { listenerFn(this.projects.slice()); } } } const projectState = ProjectState.getInstance();
-
- To split the project into several little files,
- We first have to enable this feature in our
tsconfig.json
"outFile": "./dist/bundle.js" /* Concatenate and emit output to single file. */,
- where we specify the name and directory of the output file.
- in our case we are outputting as
bundle.js
, so in ourindex.html
we have to change theapp.js
tobundle.js
- with
outFile
enabled, TypeScript will combine all.ts
files into a single filebundle.js
- with
- After creating a new
.ts
file, we then can use thenamespace
functionality (only available to TypeScript, not to JavaScript/browser) - Though this is a very dangerous way to import/export files, because if we delete something from a file, this could break our app, because we are not implicit importing the properties.
-
We can export any type of
class
,methods
,interfaces
..., we just need addexport
in front of the property -
for example, our
AutoBind
function- in
decorators/AutoBind.ts
(our end file) - We can create namespace and export the
AutoBind
function like this:
namespace App { export function AutoBind( _: any, _2: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; // store our original method const adjustedDescriptor: PropertyDescriptor = { configurable: true, get() { const boundFn = originalMethod.bind(this); return boundFn; }, }; return adjustedDescriptor; } }
- in
-
And to import our
.ts
files -
we can do so, using
///<reference parth="path/name-of-file.ts" />
-
for example in our
project-list.ts
/// <reference path="base-component.ts"/> /// <reference path="../decorators/autobind.ts"/> /// <reference path="../state/project-state.ts"/> /// <reference path="../models/project.ts"/> /// <reference path="../models/drag-drop.ts"/> namespace App { export class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget { assignedProjects: Project[]; constructor(private type: 'active' | 'finished') { super('project-list', 'app', false, `${type}-projects`); this.assignedProjects = []; this.configure(); this.renderContent(); } @AutoBind dragOverHandler(event: DragEvent) { if ( event.dataTransfer && event.dataTransfer.types[0] === 'text/plain' ) { event.preventDefault(); const listEl = this.element.querySelector('ul')!; listEl.classList.add('droppable'); } } @AutoBind dropHandler(event: DragEvent) { const projectId = event.dataTransfer!.getData('text/plain'); projectState.moveProject( projectId, this.type === 'active' ? ProjectStatus.Active : ProjectStatus.Finished ); } @AutoBind dragLeaveHandler(_: DragEvent) { const listEl = this.element.querySelector('ul')!; listEl.classList.remove('droppable'); } configure() { this.element.addEventListener('dragover', this.dragOverHandler); this.element.addEventListener('dragleave', this.dragLeaveHandler); this.element.addEventListener('drop', this.dropHandler); projectState.addListener((projects: Project[]) => { const relevantProjects = projects.filter((prj) => { if (this.type === 'active') { return prj.status === ProjectStatus.Active; } return prj.status === ProjectStatus.Finished; }); this.assignedProjects = relevantProjects; this.renderProjects(); }); } renderContent() { const listId = `${this.type}-projects-list`; this.element.querySelector('ul')!.id = listId; this.element.querySelector( 'h2' )!.textContent = `${this.type.toLocaleUpperCase()} PROJECTS`; } private renderProjects() { const listEl = document.getElementById( `${this.type}-projects-list` )! as HTMLUListElement; listEl.innerHTML = ''; for (const projectItem of this.assignedProjects) { new ProjectItem( this.element.querySelector('ul')!.id, projectItem ); } } } }
- we first have to change our
tsconfig.json
- we have to change the
"module": "amd"
to"module": "ES2015"
(when they introduced ES Modules) - And then we have to comment it out the
"outFile": "./dist/bundle.js"
because is not support with ES Modules - After all the modifications, in our
index.html
- We have to change back to
app.js
- remove the
defer
- add
type="module"
<script type="module" src="dist/app.js"></script>
- We have to change back to
-
Another and better option is to use the
ES6
modules, supported by all modern browsers. Where we simplyexport
the the thing that we want to use in another file. -
for example, in our
decorators/AutoBind.ts
export function AutoBind(_: any, _2: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; // store our original method const adjustedDescriptor: PropertyDescriptor = { configurable: true, get() { const boundFn = originalMethod.bind(this); return boundFn; }, }; return adjustedDescriptor; }
-
to import we import like we normally do using ES6
import { ... } from 'path/name-of-the-file.js'
- Important we have to define like the file has been already compiled
.js
-
in our
project-list.ts
import { AutoBind } from '../decorators/autobind.js'; import { DragTarget } from '../models/drag-drop.js'; import { Project, ProjectStatus } from '../models/project.js'; import { projectState } from '../state/project-state.js'; import { Component } from './base-component.js'; import { ProjectItem } from './project-item.js'; export class ProjectList extends Component<HTMLDivElement, HTMLElement> implements DragTarget { assignedProjects: Project[]; constructor(private type: 'active' | 'finished') { super('project-list', 'app', false, `${type}-projects`); this.assignedProjects = []; this.configure(); this.renderContent(); } @AutoBind dragOverHandler(event: DragEvent) { if ( event.dataTransfer && event.dataTransfer.types[0] === 'text/plain' ) { event.preventDefault(); const listEl = this.element.querySelector('ul')!; listEl.classList.add('droppable'); } } @AutoBind dropHandler(event: DragEvent) { const projectId = event.dataTransfer!.getData('text/plain'); projectState.moveProject( projectId, this.type === 'active' ? ProjectStatus.Active : ProjectStatus.Finished ); } @AutoBind dragLeaveHandler(_: DragEvent) { const listEl = this.element.querySelector('ul')!; listEl.classList.remove('droppable'); } configure() { this.element.addEventListener('dragover', this.dragOverHandler); this.element.addEventListener('dragleave', this.dragLeaveHandler); this.element.addEventListener('drop', this.dropHandler); projectState.addListener((projects: Project[]) => { const relevantProjects = projects.filter((prj) => { if (this.type === 'active') { return prj.status === ProjectStatus.Active; } return prj.status === ProjectStatus.Finished; }); this.assignedProjects = relevantProjects; this.renderProjects(); }); } renderContent() { const listId = `${this.type}-projects-list`; this.element.querySelector('ul')!.id = listId; this.element.querySelector( 'h2' )!.textContent = `${this.type.toLocaleUpperCase()} PROJECTS`; } private renderProjects() { const listEl = document.getElementById( `${this.type}-projects-list` )! as HTMLUListElement; listEl.innerHTML = ''; for (const projectItem of this.assignedProjects) { new ProjectItem(this.element.querySelector('ul')!.id, projectItem); } } }
-
It's a bundling and "Build Orchestration" tool, that reduces the amount of HTTP requests by bundling code together so we can write code, split across multiple files, but then webpack takes all these files and bundles together, and also optimize our code (minified).
-
At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.
-
Dependency Graph
- Any time one file depends on another, webpack treats this as a dependency. This allows webpack to take non-code assets, such as images or web fonts, and also provide them as dependencies for your application.
When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.
-
-
Install webpack as a developer tool
npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader
-
webpack
- It's the heart of webpack, this is responsible for bundling our code, plugin certain functionalities and to transform our code.
- Transform means, that webpack will take all of
.ts
files, generate the.js
and bundle all.js
into a single.js
file
- Transform means, that webpack will take all of
- It's the heart of webpack, this is responsible for bundling our code, plugin certain functionalities and to transform our code.
-
webpack-cli
- To run commands in our project
-
webpack-dev-server
- To have a builtin development server, to starts webpack under the hood which watches our files changes
-
ts-loader
- Works together with
webpack
, ts-loader tellswebpack
how to convert the TypeScript code to JavaScript
- Works together with
-
typescript
- It's a good practice to install a specific version of TypeScript per project, just in case the latest version breaks something in our project
-
Set the :
target
toes5
ores6
module
toES2015
orES6
outDir
to our destination folder (./dist
)- comment it out -
rootDir
since we don't need to specify the root directory anymore, because webpack will take over for us
{ "compilerOptions": { /* Basic Options */ "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "ES2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "lib": [ "DOM", "ES6", "DOM.Iterable", "ScriptHost" ] /* Specify library files to be included in the compilation. */, "sourceMap": true /* Generates corresponding '.map' file. */, "outDir": "./dist" /* Redirect output structure to the directory. */, // "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, "removeComments": true /* Do not emit comments to output. */, "noEmitOnError": true, /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, /* Module Resolution Options */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, /* Experimental Options */ "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ } }
-
Create a new file
webpack.config.js
in the root of our project (next totsconfig.json
) -
basically webpack uses the
node.js
features, so we can export like innode.js
export.modules = {}
- The idea is to export a JavaScript object with our webpack configuration
-
ATTENTION with webpack we don't need to specify the extension of our imports, because webpack will automatically look for files and their extensions. We had to that before because we were using the native builtin browser es module functionality
-
Define our entry point file of our project, in our
app.ts
(we use the relative path) -
the
output
property is an object, where we define thefilename
and thepath
(the path in this case is the absolute path)- to build one absolute path we can use
path
module that comes withnode.js
- set our
publicPath
todist
- We have to specify the
dist
folder, otherwise, webpack will try to look for ourbundle.js
where is called (package.json
)
- We have to specify the
- to build one absolute path we can use
-
the
devtool
property works with oursourceMap
intsconfig.json
to help us debug our codedevtool: 'inline-source-map'
-
By default webpack doesn't know what to do with
.ts
files, it only knows that has to bundle them, for that we have to tell webpack- to do that, we add the
module
property, it's an object that tells webpack how to handle our.ts
files - We define the
test
property, webpack will check the file for errors, and we define using a regular expression - then we define the
use
property, we specify what webpack should do with these files, we specify the the loader, in our casets-loader
- and for last (not necessary),
exclude: /node_modules/
- to do that, we add the
-
our last configuration
resolve
property, there we can specify webpack to look for certain types of files, by default webpack will look for '.js` files -
At the top of our configuration, add
mode: 'development',
this tells webpack that here we are build for development, and here it will do fewer optimization, to improve our debugging experience (more easier), and give more meaningful error messagesconst path = require('path'); module.exports = { mode: 'development', entry: './src/app.ts', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: 'dist' }, devtool: 'inline-source-map', module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.js', '.ts'], }, };
-
To run our build, we can add a new script inn our
package.json
"scripts": { "start": "lite-server", "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" },
-
Create a new file
webpack.config.prod.js
-
For production we have to tweak a little bit our webpack dev config
- Change
mode
toproduction
- Remove
publicPath
(that was necessary fordevelopment
) - Change
devetool
tonone
since we don't want to debug our production project - Adde an extra property, the
plugins
, it's an array, where we can define certain plugins to help us manage our webpack- For that we can install
npm i --save-dev clean-webpack-plugin
, that helps us clean up thedist
folder, it'll delete all the files in thedist
folder and build a new version of it (more updated version) whenever we rebuild our project - In the plugins array, we instantiate the
CleanPlugin
- For that we can install
const path = require('path'); const CleanPlugin = require('clean-webpack-plugin'); module.exports = { mode: 'production', entry: './src/app.ts', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, devtool: 'none', module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.js', '.ts'], }, plugins: [ new CleanPlugin.CleanWebpackPlugin() ] };
- Change
-
To run our production build we have to add
--config <name_of_the_file>
in ourscripts
inpackage.json
"scripts": { "start": "webpack-dev-server", "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack --config webpack.config.prod.js" },
-
Create a new folder
5_3rd_Party_Libraries_and_TypeScript
cd 5_3rd_Party_Libraries_and_TypeScript
npm init
npm i
tsc --init
-
Install webpack dev dependencies
npm i --save-dev typescript ts-loader webpack webpack-cli webpack-dev-server
-
Create the following folder and files
touch src/app.ts index.html webpack.config.js
-
Project structure
. ├── src │ └── app.ts ├── index.html ├── package-lock.json ├── package.json ├── tsconfig.json └── webpack.config.js
-
Add a html boilerplate
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Understanding TypeScript</title> <script src="dist/bundle.js" defer></script> </head> <body> <h1>Project Base</h1> </body> </html>
-
After installing all dev dependencies
-
webpack-dev-server
as ourstart
script{ "name": "5_3rd_party_libraries_and_typescript", "version": "1.0.0", "description": "3rd Party Libraries and TypeScript", "main": "app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "webpack-dev-server" }, "author": "Roger Takeshita", "license": "ISC", "devDependencies": { "ts-loader": "^8.0.0", "typescript": "^3.9.6", "webpack": "^4.43.0", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0" } }
-
Config our
tsconfig.json
base{ "compilerOptions": { /* Basic Options */ "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "lib": [ "DOM", "ES6", "DOM.Iterable", "ScriptHost" ] /* Specify library files to be included in the compilation. */, "sourceMap": true /* Generates corresponding '.map' file. */, "outDir": "./dist" /* Redirect output structure to the directory. */, "removeComments": true /* Do not emit comments to output. */, "noEmitOnError": true, /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, /* Module Resolution Options */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, /* Advanced Options */ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "exclude": ["node_modules"] }
-
Config our webpack development environment
const path = require('path'); module.exports = { mode: 'development', entry: './src/app.ts', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), publicPath: 'dist', }, module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.js', '.ts'], }, };
- Installing a JavaScript library (only available to JavaScript, not to TypeScript), during the compilation TypeScript will get an error. Even though during the run time we don't, since TypeScript will be transformed into JavaScript. (For that we have to comment it out the
"noEmitOnError": true,
) - Another option would be, installing a translation (types), for example
lodash
, we could install@types/lodash
and save as a dev dependencies (since we are only using during the development mode)- Basically the
@types/lodash
is a translation from plain JavaScript to TypeScript, they contain instructions to TypeScript, how this thing works and what is included in this package [file_name].d.ts
, thed
means declaration- They don't contain any logic that runs, but they define the
types
that we get back when we call a method, and so on...
- Basically the
-
One way to utilize global variables for example from our
index.html
- Even though we know JavaScript will defer the
bundle.js
until our page is loaded - This means that
var GLOBAL
will be available to global window object- But TypeScript doesn't like it
- One option is to
declare
type to inform TypeScript to not to worry about this variable.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Understanding TypeScript</title> <script src="dist/bundle.js" defer></script> </head> <body> <h1>Project Base</h1> <script> var GLOBAL = "THIS IS A GLOBAL VARIABLE FROM INDEX.HTML" </script> </body> </html>
- Even though we know JavaScript will defer the
-
in
app.ts
import _ from 'lodash'; console.log(_.shuffle([1, 2, 3])); declare var GLOBAL: any; console.log(GLOBAL);
-
Creating a class using TypeScript
-
We can create and export a class on a separate file
-
in
products.model.ts
export class Product { title: string; price: number; constructor(t: string, p: number) { this.title = t; this.price = p; } getInformation() { return [this.title, `${this.price}`]; } }
-
in
app.ts
- then we can import
Product
from./product.model
- now we can create new product using our imported class
- With our new product
p1
we have availablegetInformation()
method from our class
import { Product } from './product.model'; const p1 = new Product('A Book', 12.99); console.log(p1.getInformation());
- But the real problem is when we are getting information from a server
- Getting the products from the server, it has the same object structure, but it doesn't have the the method getInformation()
- For that, we have to loop through the entire list to create an instance of a Product (class)
const products = [ { title: 'A Carpet', price: 29.99 }, { title: 'A Book', price: 10.99 }, ]; const loadedProducts = products.map((product) => { return new Product(product.title, product.price); }); for (const product of loadedProducts) { console.log(product.getInformation()); }
- then we can import
-
One option is to install two packages
npm i class-transformer reflect-metadata
reflect-metadata
is a dependency ofclass-transform
- Class-Transform Official Docs
- To use we just, have to import
plainToClass
the most important method fromclass-transformer
and its dependencyreflect-metadata
- We just need to create call the method
plainToClass
- The first argument is the class that we want to convert to
- the second argument is the data that we want to transform
- We just need to create call the method
const loadedProducts2 = plainToClass(Product, products); for (const product of loadedProducts2) { console.log(product.getInformation()); }
npm i class-validator
-
we first have to enable
experimentalDecorators
-
in our
tsconfig.json
{ "compilerOptions": { ... /* Experimental Options */ "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, }, "exclude": ["node_modules"] }
-
to use this package, we have to add as a declarator factory (we always have to execute them, adding
()
) to our class -
in
product.model.ts
- We import all the methods that we need from
class-transform
import { IsNotEmpty, IsNumber, IsPositive } from 'class-validator'; export class Product { @IsNotEmpty() title: string; @IsNumber() @IsPositive() price: number; constructor(t: string, p: number) { this.title = t; this.price = p; } getInformation() { return [this.title, `${this.price}`]; } }
- We import all the methods that we need from
-
in
app.ts
- Just by adding the decorators to our class doesn't do the job
- We have to import
validate
fromclass-validator
- Then we create a new product, and then we call the method
validate
validate
returns a promise and always theerrors
the only difference is if the length oferrors
is equal to 0, this means there is no error.
- Then we create a new product, and then we call the method
import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; import 'reflect-metadata'; import { Product } from './product.model'; const newProduct = new Product('', -5.99); validate(newProduct).then((errors) => { if (errors.length > 0) { console.log('VALIDATION ERRORS'); console.log(errors); } else { console.log(newProduct.getInformation()); } });
-
To initialize our project with type script we can run the follow command to create a new project with TypeScript
npx create-react-app . --typescript
- the
.
means that we want to create a new react project inside of the current folder (and not create a new folder)
- the
-
But this command not always work, one work around is to install
create-react-app
globallynpm i -g create-react-app
-
Now to create a new react project we don't need the
npx
in the beginning of the commandcreate-react-app . --typescript
-
The initial structure will be:
. ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png <--- Remove │ ├── logo512.png <--- Remove │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css <--- Remove │ ├── App.test.tsx <--- Remove │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg <--- Remove │ ├── react-app-env.d.ts │ ├── serviceWorker.ts <--- Remove │ └── setupTests.ts <--- Remove ├── .gitignore ├── package-lock.json ├── package.json ├── README.md └── tsconfig.json
-
Clean our initial class component using typescript
- Remove the
logo
import from./logo.svg
that we previously deleted it - Remove the
./App.css
import
- Remove the
-
Where our class component has a type
React.FC
(React Function Component)import React from 'react'; const App: React.FC = () => { return ( <div> <h1>App</h1> </div> ); } export default App;
-
Remove the
serviceWorker
import -
Remove the
serviceWorker.unregister()
-
What is service worker in react.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import './index.css'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
-
Clean our
index.css
and use only the basic stylehtml { font-family: sans-serif; } body { margin: 0; }
-
Create the following folder and files
touch -n src/components/NewTodo.css + NewTodo.tsx + TodoList.css + TodoList.tsx src/todo.model.ts
7_React_with_TypeScript ├─ package-lock.json ├─ package.json ├─ public │ ├─ favicon.ico │ ├─ index.html │ ├─ manifest.json │ └─ robots.txt ├─ src │ ├─ App.tsx │ ├─ components │ │ ├─ NewTodo.css │ │ ├─ NewTodo.tsx │ │ ├─ TodoList.css │ │ └─ TodoList.tsx │ ├─ index.css │ ├─ index.tsx │ ├─ react-app-env.d.ts │ └─ todo.model.ts └─ tsconfig.json
-
In our
TodoList
we created a custom interfaceTodoListProps
- We created an item
items
and assigned anarray of object
as a type - the object will have the following structure an
id
typestring
and atext
typestring
- and also will have another item
onDeleteTodo
that is a function that received anid
typestring
and will in the end returnvoid
import React from 'react'; import './TodoList.css'; interface TodoListProps { items: { id: string; text: string }[]; onDeleteTodo: (id: string) => void; } const TodoList: React.FC<TodoListProps> = ({ items, onDeleteTodo }) => { return ( <ul> {items.map((todo) => ( <li key={todo.id}> <span>{todo.text}</span> <button onClick={() => onDeleteTodo(todo.id)}> DELETE </button> </li> ))} </ul> ); }; export default TodoList;
- We created an item
-
We also imported a separate css file (
TodoList.css
)ul { list-style: none; width: 90%; max-width: 40rem; margin: 2rem auto; padding: 0; } li { margin: 1rem 0; padding: 1rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26); border-radius: 6px; display: flex; justify-content: space-between; align-items: center; }
-
for our
NewTodo
we created a newtype
NewTodoProps, it could also be created as an interface- With interface we can only use to describe the structure of an object. While a type, it can be used to store other things like
union types
(multiple types into one type) let text: string | string[]; - When we define as an interface, it's clear that
we want to define only the structure of the object
, while type is not always truetype
import React, { useRef } from 'react'; import './NewTodo.css'; type NewTodoProps = { onAddTodo: (todoText: string) => void; }; const NewTodo: React.FC<NewTodoProps> = ({ onAddTodo }) => { const textInputRef = useRef<HTMLInputElement>(null); const todoSubmitHandler = (event: React.FormEvent) => { event.preventDefault(); const enteredText = textInputRef.current!.value; onAddTodo(enteredText); }; return ( <form onSubmit={todoSubmitHandler}> <div className="form-control"> <label htmlFor="todo-text">Todo Text</label> <input type="text" id="todo-text" ref={textInputRef} /> </div> <button type="submit">ADD TODO</button> </form> ); }; export default NewTodo;
- With interface we can only use to describe the structure of an object. While a type, it can be used to store other things like
-
in our
NewTodo.css
form { width: 90%; max-width: 40rem; margin: 2rem auto; } .form-control { margin-bottom: 1rem; } label, input { display: block; width: 100%; } label { font-weight: bold; } input { font: inherit; border: 1px solid #ccc; padding: 0.25rem; } input:focus { outline: none; border-color: #50005a; } button { background: #50005a; border: 1px solid #50005a; color: white; padding: 0.5rem 1.5rem; cursor: pointer; } button:focus { outline: none; } button:hover, button:active { background: #6a0a77; border-color: #6a0a77; }
-
Create an interface to use as model of structure for our todo item
export interface Todo { id: string; text: string; }
-
in our
App.tsx
we import all the component and model tha we created -
import
useState
fromreact
to manage our state-
By default TypeScript doesn't know the
type
ofuseState
(by default it's a generic type) -
That's why we created our interface (
Todo
), so we can assign/indicate to TypeScript that our[todos, setTodos]
is an array of typeTodo
(an array of objects ({id, text}))import React, { useState } from 'react'; import NewTodo from './components/NewTodo'; import TodoList from './components/TodoList'; import { Todo } from './todo.model'; const App: React.FC = () => { const [todos, setTodos] = useState<Todo[]>([]); const todoAddHandler = (text: string) => { setTodos((prevTodos) => [ ...prevTodos, { id: Math.random().toString(), text }, ]); }; const todoDeleteHandler = (todoId: string) => { setTodos((prevTodos) => { return prevTodos.filter((todo) => todo.id !== todoId); }); }; return ( <div> <NewTodo onAddTodo={todoAddHandler} /> <TodoList items={todos} onDeleteTodo={todoDeleteHandler} /> </div> ); }; export default App;
-
-
To start a new project, first we initialize a new node environment and then initialize TypeScript
npm init tsc --init npm i express body-parser morgan npm i --save-dev @types/node @types/express @types/morgan
-
- Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications.
-
- Node.js body parsing middleware.
- Parse incoming request bodies in a middleware before your handlers, available under the req.body property.
- Note As req.body's shape is based on user-controlled input, all properties and values in this object are untrusted and should be validated before trusting. For example, req.body.foo.toString() may fail in multiple ways, for example the foo property may not be there or may not be a string, and toString may not be a function and instead a string or other user input.
-
-
Set up the base TypeScript configuration
-
Add a some extra configuration
"modeResolution": "node"
- This simple tells TypeScript how different files and imports work together
"noEmitOnError": true
- To not compile if any errors
"exclude": ["node_modules"]
- To implicit indicate to exclude
node_modules
from TypeScript compilation
- To implicit indicate to exclude
{ "compilerOptions": { "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, "moduleResolution": "node", "outDir": "./dist" /* Redirect output structure to the directory. */, "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, "removeComments": true /* Do not emit comments to output. */, "noEmitOnError": true, "strict": true /* Enable all strict type-checking options. */, // "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, "exclude": ["node_modules"] }
-
In our
package.json
we need to configure our start script{ "name": "8_node_express_typescript", "version": "1.0.0", "description": "Node Express with TypeScript", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "nodemon dist/index.js" }, "author": "Roger Takeshita", "license": "ISC", "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1" }, "devDependencies": { "@types/express": "^4.17.7", "@types/morgan": "^1.9.1", "@types/node": "^14.0.24", "morgan": "^1.10.0" } }
-
Create the the following structure
touch src/app.ts + index.ts
-
Basic server structure
8_Node_Express_TypeScript ├─ dist │ ├─ app.js │ └─ index.js ├─ package-lock.json ├─ package.json ├─ src │ ├─ app.ts │ └─ index.ts └─ tsconfig.json
-
In app ts we are going to:
- Import
express
fromexpress
- Import
logger
frommorgan
- Create an instance of
express()
(our server) - Use logger in
dev
mode, so we can see the incoming requests on our terminal - Use
express.json()
for parsing application/json - Create a default route if route not found
- export the app
import express from 'express'; import logger from 'morgan'; const app = express(); app.use(logger('dev')); app.use(express.json()); app.get('/*', (req, res) => { res.status(404).json({ message: "Path doesn't exist" }); }); export default app;
- Import
-
In
index.ts
we need to:- Import
app
from./app
- Create a new port constant
- Create a listener to our app
import app from './app'; const port = process.env.PORT || 3001; app.listen(port, () => { console.log(`Server is running on port ${port}`); });
- Import
-
Create the following folders and files
touch src/controllers/todos.ts src/models/todo.ts src/routes/todos.ts
8_Node_Express_TypeScript ├─ package-lock.json ├─ package.json ├─ src │ ├─ app.ts │ ├─ controllers │ │ └─ todos.ts │ ├─ index.ts │ ├─ models │ │ └─ todo.ts │ └─ routes │ └─ todos.ts └─ tsconfig.json
-
In
src/app.ts
- Import the request types (
NextFunction, Request, Response
) fromexpress
so we can indicate to TypeScript more precise type, instead of the generic type - Import the
todos
routes, so we can assign a specific route to them - Create an error handler
import express, { NextFunction, Request, Response } from 'express'; import logger from 'morgan'; import todoRoutes from './routes/todos'; const app = express(); app.use(logger('dev')); app.use(express.json()); app.use('/todos', todoRoutes); app.use((error: Error, req: Request, res: Response, next: NextFunction) => { res.status(500).json({ message: error.message }); }); app.get('/*', (req, res) => { res.status(404).json({ message: "Path doesn't exist" }); }); export default app;
- Import the request types (
-
in
src/models/todo.ts
- Let's create a structure of our todo (type class)
export class Todo { constructor(public id: string, public text: string) {} }
-
in
controllers/todos.ts
- Import
RequestHandler
fromexpress
- It's the same of importing
Request, Response, NextFunction
the only difference it's all in one type
- It's the same of importing
- Import
Todo
structure from ourmodels
- Create all CRUD Operations
- For incoming data, TypeScript doesn't know the type, if we know the type, we could create a
type casting
like so:const text = (req.body as { text: string }).text;
- Where the incoming data has a body with
text
field typestring
import { RequestHandler } from 'express'; import { Todo } from '../models/todo'; //! Fake Database const LIST_TODOS: Todo[] = []; const createTodo: RequestHandler = (req, res, next) => { const text = (req.body as { text: string }).text; //+ Add type casting const newTodo = new Todo(Math.random().toString(), text); LIST_TODOS.push(newTodo); res.status(201).json({ message: 'Created the todo.', createTodo: newTodo }); }; const getTodos: RequestHandler = (req, res, next) => { res.json({ todos: LIST_TODOS }); }; const updateTodo: RequestHandler<{ id: string }> = (req, res, next) => { const updatedText = (req.body as { text: string }).text; const todoIndex = LIST_TODOS.findIndex((todo) => todo.id === req.params.id); if (todoIndex < 0) throw new Error('Could not find todo.'); LIST_TODOS[todoIndex] = new Todo(LIST_TODOS[todoIndex].id, updatedText); res.json({ message: 'Updated Successfully!', updatedTodo: LIST_TODOS[todoIndex], }); }; const deleteTodo: RequestHandler = (req, res, next) => { const todoIndex = LIST_TODOS.findIndex((todo) => todo.id === req.params.id); if (todoIndex < 0) throw new Error('Could not find todo.'); LIST_TODOS.splice(todoIndex, 1); res.json({ message: 'Todo has been deleted!' }); }; export { createTodo, getTodos, updateTodo, deleteTodo };
- Import
-
In
routes/todos.ts
- Different from a normal node/express server (without TypeScript), the way we use express
Router
is:
const express = require('express'); const router = express.Router();
- With TypeScript, we no longer have to import
express
, we just importRouter
directly fromexpress
;
import { Router } from 'express'; import { createTodo, deleteTodo, getTodos, updateTodo, } from '../controllers/todos'; const router = Router(); router.post('/', createTodo); router.get('/', getTodos); router.patch('/:id', updateTodo); router.delete('/:id', deleteTodo); export default router;
- Different from a normal node/express server (without TypeScript), the way we use express