- 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 init
to set up thepackage.json
file. - Use
npm install --saveDev lite-server
to 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 start
to start serving your website.- Now the app will be automatically reloaded.
Having both the
.js
and the.ts
files open at the same time might generate errors due to conflicts in the IDE.
number
string
boolean
- 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.
Array
let 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'; // error
type 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 --watch
What if you have more than 1 file? Use this to initiate a configuration for the project:
tsc --init
tsc -w
Add 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
:es5
is the default — it doesn't havelet
andconst
.lib
: The default contains thedom
library for browsers for example."lib": [ "dom", "es6", "dom.iterable", "scripthost" ],
sourceMap
s: iftrue
, will enable.ts
files in the browser for debugging.rootDir
: Typically, thedist
folder will have the output and thesrc
folder will have the TS files."outDir": "./dist/", "rootDir": "./src/",
removeComments
is a good option for memory optimization.noEmit
won't compile to JS, so the workflow will be simpler.downlevelIteration
will limit the iteration loops, which will output more robust code.noEmitOnError
(default isfalse
). Setting it totrue
will be safer and won't generate broken code.strict
is 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...
strictNullChecks
document.querySelector('button')!
might benull
at some point. And that's why we add the!
.
strictFunctionTypes
strictBindCallApply
: this is related to binding thethis
keyword.function clickHandler(message: string) { console.log('Clicked! ' + message); } if (button) { button.addEventListener('click', clickHandler.bind(null, 'Youre welcome')); }
Additional Checks
increase code quality.
Extensions:
- ESLint
- npm
- Prettier
- Debugger for Chrome
- Enable the
sourceMap
option 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.
var
has global and function scope.- Declaring a
var
inside anif
block, for example, also creates avar
globally in JS.- TS would complain anyway...
- Declaring a
let
only 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 pointer
An 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 properties
class 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:
in
instanceof
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 solution
Same 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 booleans
Generics are more flexible with the types, while unions are more flexible.
Metaprogramming
Don't forget to enable
experimentalDecorators
in 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 file
Importing 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
target
is ates5
ores6
. module
should be set toes6
+.- Check your
outDir
. - Comment the
rootDir
. - Create a
webpack.config.js
file 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.exports
is how you export in NodeJS.
- Add to the
scripts
:"build": "webpack"
- Run
npm run build
- Simply replace the
start
key with"webpack-dev-server"
. - Add to
module.exports -> output
publicPath: 'dist'
. - Add to
module.exports
mode: 'development'
.
- Create a
webpack.config.prod.js
- Webpack doesn't care about this file, name it however you want.
- Copy the dev configurations.
- Alter
mode
to'production'
. - Set
devtool
to'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
build
key:"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
lodash
was 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/googlemaps
to 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/express
import 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.