- Static error checking is always better than runtime crazy ones.
- Getting Started
- TypeScript Basics
- Compiler & Configuration Deep Dive
- Working with Next-Gen JS Code
- Classes & Interfaces
- Advanced Types & TypeScript Features
- Generics
- Decorators
- Time to Practice — Full Project
- Working with Namespaces & Modules
- Webpack & TypeScript
- Third-Party Libraries & TypeScript
- React + TS & NodeJS + TS
- Use
npm initto set up thepackage.jsonfile. - Use
npm install --saveDev lite-serverto install a dependency which will only affect the development environment. - Add
"start": "lite-server"to the"scripts"key.- And also add this key:
"devDependencies": { "lite-server": "^2.5.4" }
- And also add this key:
- Use
npm startto start serving your website.- Now the app will be automatically reloaded.
Having both the
.jsand the.tsfiles open at the same time might generate errors due to conflicts in the IDE.
numberstringboolean- Truthy and falsy values still exist...
object- Object types are inferred if you're creating one directly.
const person1 = { name: 'Max', // inferred as `string` age: 30 // inferred as `number` } const person2: { name: string; age: number; } = { name: 'Max', age: 30 }
- Object types are inferred if you're creating one directly.
Arraylet favoritActivities: string[]; favoritActivities = ['Sports']; for (const hobby of favoriteActivities) { console.log(hobby.toUpperCase()); }
Tuple: fixed-length and fixed-type array.- Be careful with implicit tuples, they can be inferred as unions (
|).
const role: [number, string] = [2, 'author'];
- The
push()method is an exception for types.
- Be careful with implicit tuples, they can be inferred as unions (
Enum- Considered to be a custom type.
enum Role { new = 5, old }
Any- Takes away the benefits of TS, it's basically JS now.
Use typeof to check types:
if (typeof n1 === 'number') { ... }const eitherXOrY : 'x' | 'y' = 'x';
const eitherXOrY : 'x' | 'y' = 'z'; // errortype Combinable = number | string;void is exclusive to function return types.
function add(n1: number, n2: number): number {
return n1 + n2;
}
function printResult(num: number): void {
console.log(num);
}
function printResult(num: number): undefined {
console.log(num);
return; // `undefined` return type
}One = + one ! includes both null and undefined => !=.
let combinedValues: Function; // won't have parameters and return values typed
let combinedValues: (a: number, b: number) => number;
combinedValues = add;void means TS won't care about what you're returning.
The better choice over any.
let userInput: unknown;
let userName: string;
userInput = 5;
userInput = 'Max';
if (typeof userInput === 'string') { // `unknown` needs a check
userName = userInput;
}This function returns a never:
function generateError(message: string, code: number): never {
throw {message: message, errorCode: code};
// while (true) {}
}tsc file.ts --watchWhat if you have more than 1 file? Use this to initiate a configuration for the project:
tsc --init
tsc -wAdd this to the end of tsconfig.json:
"exclude": [
"analytics.ts",
"*.dev.ts",
"node_modules"
],
"include": [ // If in the config, you have to specify everything.
"app.ts",
],
"files": [ // Can't specify folders here.
"app.ts"
]"node_modules" is automatically excluded actually.
target:es5is the default — it doesn't haveletandconst.lib: The default contains thedomlibrary for browsers for example."lib": [ "dom", "es6", "dom.iterable", "scripthost" ],
sourceMaps: iftrue, will enable.tsfiles in the browser for debugging.rootDir: Typically, thedistfolder will have the output and thesrcfolder will have the TS files."outDir": "./dist/", "rootDir": "./src/",
removeCommentsis a good option for memory optimization.noEmitwon't compile to JS, so the workflow will be simpler.downlevelIterationwill limit the iteration loops, which will output more robust code.noEmitOnError(default isfalse). Setting it totruewill be safer and won't generate broken code.strictis the same as setting up all of the options below it (inside the strict block).noImplicitAny- Sometimes it isn't possible for TypeScript to infer types...
strictNullChecksdocument.querySelector('button')!might benullat some point. And that's why we add the!.
strictFunctionTypesstrictBindCallApply: this is related to binding thethiskeyword.function clickHandler(message: string) { console.log('Clicked! ' + message); } if (button) { button.addEventListener('click', clickHandler.bind(null, 'Youre welcome')); }
Additional Checksincrease code quality.
Extensions:
- ESLint
- npm
- Prettier
- Debugger for Chrome
- Enable the
sourceMapoption insidetsconfig.json. - Press F5 and choose Chrome to start an anonymous debugging session.
- You can even place breakpoints.
- Enable the
Using Next-Gen JS Syntax
Checking which features work where.
The difference is the scope.
varhas global and function scope.- Declaring a
varinside anifblock, for example, also creates avarglobally in JS.- TS would complain anyway...
- Declaring a
letonly has block scope.- It's only available in the block you wrote or in lower level ones.
const person = {
name: 'Max',
age: 30
};
const copiedPerson = { ...person }; // a different object, not a pointerAn unlimited amount of parameters.
const add = (...numbers: number[]) => {
return numbers.reduce((curResult, curValue) => {
return curResult + curValue;
}, 0);
};
const addNumbers = add(5, 10, 2, 3.7);Similar to Python...
const [hobby1, hobby2, ...remainingHobbies] = hobbies; // doesn't change the original, just copies
const { firstname: userName, age } = person; // the names have to be the propertiesclass Department {
name: string;
constructor(n: string) {
this.name = n;
}
}
const accounting = new Department('Accounting');The rule of thumb is that describe below will call on the immediate object and not necessarily on the correct one.
class Department {
private name: string;
private employees: string[] = [];
constructor(n: string) {
this.name = n;
}
describe(this: Department) {
console.log('Department: ' + this.name);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department('Accounting');
const accoutingCopy = { describe: accounting.describe };
accountingCopy.describe(); // will cause an error, has to add a `name` property to `accountingCopy` and a `this` parameter to `describe`
accounting.addEmployee('Max');
accounting.addEmployee('Manu');
accounting.printEmployeeInformation();constructor(private id: string, public name: string) {...}Now you don't even need to mention the property outside the constructor, it will be automatically done for you.
The readonly modifier means that it won't change later.
constructor (private readonly id: string, ...) {...}The class receives the superclass' constructor by default, unless you add one.
class ITDepartment extends Department {
constructor(id: string, public admins: string[]) {
super(id, 'IT');
this.admins = admins;
}
}The property won't be accessible from outside, but it will be accessible from other subclasses.
get mostRecentReport() {
return this.lastReport;
}
set mostRecentReport(value: string) {
this.addReport(value);
}Just place the static keyword in front of the method:
static createEmployee() {}Simply add the abstract keyword:
abstract describe(): void {}You can also have abstract classes and properties. You cannot have a private abstract method.
Add the private keyword in front of the constructor:
class AccountingDepartment extends Department {
private static instance: AccountingDepartment;
private constructor() {}
static getInstance() {
if (AccountingDepartment.instance) {
return this;
} else {
this.instance = AccountingDepartment('d2', []);
return this.instance;
}
}
}You don't need to implement a class for the interface. An object literal also works:
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
let user1: Person;
user1 = {
name: 'Max',
age: 30,
greet(phrase: string) {
console.log(phrase + ' ' + this.name);
}
}We could use the type keyword above, but then we wouldn't be able to implement it in a class.
You can inherit from only 1 class, but you can implement multiple interfaces.
class Person implements Greetable {
...
}You cannot add public or private, but readonly does work.
interface Greetable {
readonly name: string;
}interface Named {
readonly name: string;
}
interface Greetable extends Named {
greet(phrase: string): void;
}// type AddFn = (a: number, b: number) => number;
interface AddFn {
(a: number, b: number): number;
}interface Named {
readonly name: string;
outputName?: string;
}
class Person implements Greetable {
name?: string;
...
}This also works for parameters.
They are not translated to JS. There is no translation.
type Admin = {
name: string;
privileges: string[];
}
type Employee = {
name: string;
startDate: Date;
}
type ElevatedEmployee = Admin & Employee;
const e1: ElevatedEmployee = {
name: 'Max',
privileges: ['create-server'],
startDate: new Date()
}You could also use interfaces with extends to achieve the same effect. Intersection types also work with union types.
Two Options:
ininstanceof
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof ===) {
return a.toString() + b.toString();
}
return a + b;
}
type UnknownEmployee = Employee | Admin;
function printEmployeeInfo(emp: UnknownEmployee) {
console.log('Name: ' + emp) // `typeof emp.privileges` won't work
if ('privileges' in emp) { // JS feature
console.log('Privileges: ' + emp.privileges);
}
}
class Car {
drive() {}
}
class Truck {
drive(){}
loadCargo(){}
}
type Vehicle = Car | Type;
const v1 = new Car();
const v2 = new Truck();
function useVehicle(vehicle: Vehicle) {
if (vehicle instanceof Truck) {
}
}interface Bird {
type: 'bird'; // literal type
flyingSpeed: number;
}
interface Horse {
type: 'horse';
runningSpeed: number;
}
type Animal = Bird | Horse;
function moveAnimal(animal: Animal) {
let speed;
// typeof won't work because interfaces are not compiled
switch (animal.type) {
case 'bird':
speed = animal.flyingSpeed;
break;
case 'horse':
speed = animal.runningSpeed;
break;
}
console.log('Moving with speed: ' + speed);
}const userInputElement = <HTMLInputElement>document.getElementById('user-input')!;
// or
const userInputElement = document.getElementById('user-input')! as HTMLInputElement;The ! tells TS that it will never be null.
"I don't know how many properties I'll have."
interface ErrorContainer {
[prop: string]: string;
}
const errorBag: ErrorContainer = {
email: 'Not a valid email',
username: 'Must start with a captial character'
}// Overloading return and parameter types
function add(a: string): string
function add(a: string, b: string): string
function add(a: number, b: number): number
function add(a: Combinable, b: Combinable) {
if (typeof a === 'string' || typeof b === 'string') {
return a.toString() + b.toString();
}
return a + b;
}
// Can't call string methods on `Combinable` because TS isn't sure it's a string.
const result = add('Max', 'Schwarz');
// const result = add('Max', 'Schwarz') as string; // one solutionSame as null-aware chaining in Dart.
const fetchedUserData = {
id: 'u1',
name: 'Max',
job: {
title: 'CEO',
description: 'My own company'
}
};
console.log(fetchedUserData.job && fetchedUserData.job.title); // The JS way
console.log(fetchedUserData?.job?.title);Null-aware asignment in Dart.
const userInput = null;
// const storedData = userInput || 'DEFAULT'; // will work weirdly if userInput is falsy but non-null (`''`)
const storedData = userInput ?? 'DEFAULT';const names: Array<string> = []; // array uses Array<T>, which needs to be specified
const promise: Promise<string> = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('This is done!');
}, 2000);
})Almost the same thing as in Dart:
function merge<T, U>(objA: T, objB: U): T & U {
return Object.assign(objA, objB);
}
const mergeObj = merge({name: 'Max'}, {age: 30}); // without generics storing it in a variable will not have `name` or `age` available
console.log(mergeObj.age);Really similar to Dart again:
function merge<T extends object, U extends object>(objA: T, objB: U): T & U {
return Object.assign(objA, objB);
}
// With constraints to the generic types, you can't pass 30 anymore
const mergeObj = merge({name: 'Max'}, 30); // How would you access the 30 then?function extractAndConvert<T extends object, U extends keyof T>(obj: T, key: U) {
return obj[key];
}class Storage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data.splice(this.data.indexOf(item), 1);
}
getItems() {
return [...this.data];
}
}interface CourseGoal {
title: string;
description: string;
completeUntil: Date;
}
function createCourseGoal(title: string, description: string, date: Date): CourseGoal {
let courseGoal: Partial<CourseGoal> = {}; // properties are going to be completed
courseGoal.title = title;
courseGoal.description = description;
courseGoal.completeUntil = date;
return courseGoal as CourseGoal
// return {title: title, description: description, date: date}
}
const names: Readonly<string> = ['Max', 'Anna'];
name.push('Manu'); // error, not allowed(string | number | boolean)[] // array with strings, numbers and booleans
// !=
string[] | number[] | boolean[] // array of only string, only numbers or only booleansGenerics are more flexible with the types, while unions are more flexible.
Metaprogramming
Don't forget to enable
experimentalDecoratorsin thetsconfig.json.
The decorator runs when TS finds the constructor definition, not necessarily when it is used.
function Logger(constructor: Function) {
console.log('Logging...');
console.log(constructor);
}
@Logger
class Person {
name = 'Max';
constructor() {
console.log('Creating person object');
}
}
const person = new Person();
console.log(person);function Logger(logString: string) {
return function(constructor: Function) {
console.log(logString );
console.log(constructor);
}
}
@Logger('Logging Person') // needs to execute
class Person {
name = 'Max';
constructor() {
console.log('Creating person object');
}
}Using decorators for HTML templates:
function WithTemplate(template: string, hookId: string) {
return function(constructor: Function) {
const hookEl = document.getElementById(hookId);
const p = new constructor(); // Now we can access the object itself.
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = p.name;
}
}
}
@WithTemplate('<h2>My Person Object</h2>', 'app')
class Person {
name = 'Max';
constructor() {
console.log('Creating person object');
}
}Anyone could import this decorator function to render HTML on their class.
This is basically how Angular uses decorators.
You can add more than 1 decorator to a class. They are executed bottom-up.
Other places where you can add decorators:
- Properties
- Accessors (
set) - Methods
- Parameters
Examine the documentation to check which parameters they should have.
They all execute when a class is defined, instance-wise.
A class is nothing more than a constructor function in the end:
function WithTemplate(template: string, hookId: string) {
return function<T extends {new(...args: any[]): {name: string}}>(originalConstructor: Function) {
return class extends constructor {
constructor(..._: any[]) {
super();
const hookEl = document.getElementById(hookId);
const p = new constructor(); // Now we can access the object itself.
if (hookEl) {
hookEl.innerHTML = template;
hookEl.querySelector('h1')!.textContent = this.name;
}
}
}
}
}Only decorators on methods and accessors can return something.
A method is just a function with a property as a value.
function Autobind(_: any, __: 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();
p.showMessage();
const button = document.querySelector('button');
// The `this` keyword with event listeners... you have to bind stuff without an autobind decorator.
// button.addEventListener('click', p.showMessage.bind(p));
button.addEventListener('click', p.showMessage);interface ValidatorConfig {
[property: string]: {
[validatableProp: string]: string[]
}
}
const registeredValidators: ValidatorConfig = {};
function Required(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: ['required']
};
}
function PositiveNumber() {
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 isValied = 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(); // preventing "No HTTP requests"
const title = 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;
}
});<form>
<input type="text" placeholder="Course title" id="title"/>
<input type="text" placeholder="Course price" id="price"/>
<button type="submit">Save</button>
</form>- typestack's
class-validator- Very nice to take a look at professional decorators.
- Angular has a lot of decorators we can import individually.
- NestJS uses decorators for the server side.
Just create a class with a private constructor.
Use the dragStart event from the browser to deal with drag & drop. You also need to add the draggable="true" to the HTML element in order for the browser to prepare itself.
interface Draggable {
dragStartHandler(event: DragEvent): void;
dragEndHandler(event: DragEvent): void;
}
interface DragTarget {
dragOverHandler(event: DragEvent): void; // otherwise dropping won't be possible
dropHandler(event: DragEvent): void;
dragLeaveHandler(event: DragEvent): void;
}
...
@autobind
dragOverHandler(_: DragEvent) {
const listEl = this.element.querySelector('ul')!;
event.preventDefault(); // otherwise dropping is not allowed
listEl.classList.add('droppable'); // this class changes the color of the background in the CSS
}
...
configure() {
this.element.addEventListener('dragover', this.dragOverHandler);
}3 options:
- Write different files and have TS compile them all to JS.
- Manual imports.
- Namespaces & File Bundling.
- Bundles multiple TS files into 1 JS files.
- Per-file or bundled compilation is possible (less imports to manage).
- ES6/Exports Modules
- JS already supports imports/exports.
- Per-file compilation but single
<script>import. - Bundling via third-party tools (e.g. Webpack) is possible.
namespace App {
export interface X {...} // without `export` they wouldn't be available outside the fileImporting the namespace — the /// are mandatory —:
/// <reference path="drag-drop-interfaces.ts" />
namespace App {
// put your file code in the same namespace but now in this file
}To bundle all the code into only one JS script, you can change outFile in the tsconfig.json and change the module key to amd.
Only work in modern browsers, like Chrome and Firefox.
Importing/exporting exactly what you want.
You can use the export keyword without the namespace keyword.
import { Draggable } from '../models/drag-drop.js;' // remember the `.js`This is more in tune with modern JS and TS.
Use ES6+ on the module key of the tsconfig.json. The outFile key is no longer supported.
And you will need to take defer out and insert module into the script element:
<script type="module" src="dist/app.js"></script>- Importing a lot of stuff
import * as Validation from '../path';
- Aliasing other files' names:
import { autobind as Autobind } from '../path';
- If you have a file that only exports one thing:
export default class A { ... } ... import Cmp from '../path'; // Choose your own name
- This is bad for name conventions though...
If you have a const in one file being imported by multiple files, how often does it execute? Only once, when the first import requires it (thankfully).
If you use JS Modules, your code will still appear in different files, so the browser will have a ton of overhead to clear unfortunately.
Webpack is a bundling & build orchestration tool.
- Normal setup
- Multiple .ts files & imports (HTTP requests)
- Unoptimized code (not as small as possible)
- External development server needed.
- With Webpack
- Code bundles, less imports required
- Optimized (minified) code, less code to download
- More build steps can be added easily
npm install --save-dev webpack webpack-cli webpack-dev-server typescript ts-loader| Package | Purpose |
|---|---|
webpack |
The heart of bundling |
webpack-cli |
Running CLI commands with Webpack |
webpack-dev-server |
For refreshing the server with the custom |
ts-loader |
How to convert TS code to JS with Webpack. |
typescript |
It's a good practice to install a copy of TS per project. |
- Make sure
targetis ates5ores6. moduleshould be set toes6+.- Check your
outDir. - Comment the
rootDir. - Create a
webpack.config.jsfile at the root of the project:const path = require('path'); module.exports = { entry: './src/app.ts', output: { filename: 'bundle.[contenthash].js', path: path.resolve(__dirname, 'dist') }, devtool: 'inline-source-map', module: { rules: [ { test: /\.ts$/, use: 'ts-loader', exclude: /node-modules/ } ] }, resolve: { extensions: ['.ts', '.js'] } };
module.exportsis how you export in NodeJS.
- Add to the
scripts:"build": "webpack"
- Run
npm run build
- Simply replace the
startkey with"webpack-dev-server". - Add to
module.exports -> outputpublicPath: 'dist'. - Add to
module.exportsmode: 'development'.
- Create a
webpack.config.prod.js- Webpack doesn't care about this file, name it however you want.
- Copy the dev configurations.
- Alter
modeto'production'. - Set
devtoolto'none'. - Run
npm install --save-dev clean-webpack-plugin - Add at the bottom:
const CleanPlugin = require('clean-webpack-plugin'); ... plugins: [ new CleanPlugin.CleanWebpackPlugin() ]
- Use on the
buildkey:"webpack --config webpack.config.prod.js" npm run build.
- Normal libraries (JS) and using them with TS.
- TS-specific libraries
- Lodash
npm -i --save-dev lodash
- TS won't understand it because
lodashwas only written for TS. - Go to the DefinitelyTyped repo for the declaration types of the modules (
.d.ts). The TS docs teach you how to do that. - You will need to install types for it:
npm install --save-dev @types/lodash
- TS won't understand it because
What if you have a global variable in your HTML <script>?
declare var GLOBAL: any;(JSON serialized data from the server)
The class-transformer package does this conversion for us.
The same goes for the class-validator package.
You could use the built-in fetch, but the axios package offers nicer support.
axios.get();The response from the Google Maps API will be a nested JSON object.
You can also specify the type of the get response:
axios.get<{results: {geometry: {location: {lat: number, lng: number}}}[]}>(...);Then:
- Install Google Maps' SDK
<script> - Use the global variable declaration to make TS aware of it:
declare var google: any;
- Then use
const map = ...with the coordinates above to place your item on the interactive Google Maps on your website. - Use
@types/googlemapsto get typing support.
Node.js is not able to execute TS code on its own. Compiling the TS code to js and using the node CLI to execute it still works though. If you try to execute your TS code, it might work if you have JS-compatible code only.
Install both types:
npm install --save-dev @types/node
npm install --save-dev @types/expressimport express from 'express';
const app = express();
app.listen(3000);import { Router } from 'express';
const router = Router();
router.post('/');
router.get('/');
router.patch('/:id');It's a Node.js-like server-side framework for TS.