TypeScript Notes
Cheatsheet
Typescript Classes
Typescript Control Flow
Typescript Interfaces
Typescript Types
Typescript notes
-
+
will convert ‘string’ variable to ‘number’ - Install typescript:
npm install typescript --save-dev
as dev dependency - Install typescript globally:
npm install -g typescript
Compiling TS
- Initialize typescript - creates
tsconfig.json
typescript --init
- Run typescript:
tsc
Useful VS extensions
- ESLint - To lint JS code
- Material Icon theme - Visual icons for files
- Path Intellisense - better support for imports
- Prettier - Formats code
Setup a fresh project with node live server
Install nodejs and npm
-
npm init
and add details - it’ll create package.json file - run
npm install --save-dev lite-server
- installs lite-server and mark it as develepment only dependencies - Add “start”: “lite-server” in package.json. It’ll give you a command
npm start
which will resolve tolite-server
. So nownpm start
will start the project. - If any changes in TS file, then need to run
tsc
Core types
Javascript uses “dynamic types” (Resolved at runtime), TypeScript uses “static types” (set during development)
-
number
- All numbers integers or floats -
string
- All texts - ‘’, “”, `` -
boolean
- true/false -
objects
- Of course object types can also be created for nested objects.
Let’s say you have this JavaScript object:
const product = {
id: "abc1",
price: 12.99,
tags: ["great-offer", "hot-and-new"],
details: {
title: "Red Carpet",
description: "A great carpet - almost brand-new!",
},
};
This would be the type of such an object:
{
id: string;
price: number;
tags: string[];
details: {
title: string;
description: string;
}
}
So you have an object type in an object type so to say. 5. Array
- [1,2] 6. Tuple
- [1,2] - Fixed length array
const person: {
name: string,
age: number,
hobbies: string[],
role: [number, string],
} = {
name: "Chandan",
age: 37,
hobbies: ["books", "technology"],
role: [2, "author"],
};
-
enum
- enum {NEW, OLD} - Enumerated global constant identifiers
enum Role {NORMAL, ADMIN, SUPERADMIN = 5}
enum MemberShipType {
FREE = 10,
GOLD = 'Gold',
PLATINUM = 'Platinum'
}
-
any
- Any type is a valid any type
Type alias
type Combinable = string | number;
type ConversionDiscriptor = "as-number" | "as-string";
function combine(
input1: Combinable,
input2: Combinable,
resultAs: ConversionDiscriptor
) {
let result;
if (
(typeof input1 === "number" && typeof input2 === "number") ||
resultAs === "as-number"
) {
result = +input1 + +input2;
} else {
result = input1.toString() + input2.toString();
}
return result;
}
Another example:
function greet(user: { name: string, age: number }) {
console.log("Hi, I am " + user.name);
}
function isOlder(user: { name: string, age: number }, checkAge: number) {
return checkAge > user.age;
}
// To:
type User = { name: string, age: number };
function greet(user: User) {
console.log("Hi, I am " + user.name);
}
function isOlder(user: User, checkAge: number) {
return checkAge > user.age;
}
function return types
// Explicit return type. If not provided, TS will infer the return type
function add(n1: number, n2: number): number {
return n1 + n2;
}
// return void
function printResult(num: number) {
console.log("Result is: ", num);
}
printResult(add(5, 10));
Unknown type
let userInput: unknown;
let userName: string;
userInput = 5;
userInput = "Chandan";
// here explicit check of typeof userInput is required as
// userInput is of type `unknown`. if it was `any` then check wasn't required.
if (typeof userInput === "string") {
userName = userInput;
}
never
type
Mostly used for functions, which never returns anything, e.g. functions throwing error
function generateError(message: string, code: number): never {
throw { message: message, errorCode: code };
}
generateError("Something went wrong", 400);
TypeScript Compiler configuration - Comprehensive doc here
watch mode - Keep tsc
in watch mode while development
$> tsc -w
$> tsc --watch
$> tsc app.ts -w
exclude/include comiling files in tsconfig.js
Files to compile = include
d files - exclude
d files
{
"compilerOptions": {
// ...
},
"exclude": [
"analytics.dev.ts",
"**/*.dev.ts", // exclude all files in all folders with `.dev.ts` extension
"node_modules" // NOTE: "node_modules" are excluded by default, but if `exclude` is added, then it must be specified.
],
"include": [ // if include specified, then only files listed will be included in complier
]
}
Compiling ts into different version of ECMAScript
You can change the target to compile to older version of javascript/ECMAScript
// In tsconfig.json
{
"compilerOptions": {
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
//...
}
}
lib - By default it contains all the libraries for interacting with “DOM” and other required libraries. Following set is by default available.
"lib": [
"DOM",
"ES6",
"DOM.Iterable",
"ScriptHost"
],
sourceMap = true
This option generated .ts.map
files, which gets rendered to browser’s source and can be used for debugging in .ts
files instead of .js
files.
sourceMap: true;
outDir: “./dist”
Specify a file that bundles all outputs into one JavaScript file. If ‘declaration’ is true, also designates a file that bundles all .d.ts output.
rootDir: “./src”
Specify the root folder within your source files. All other files outside ./src
will be not be compiled by TS.
noEmitOnError: true/false
Disable emitting files if any type checking errors are reported.
“noUnusedLocals”: true
Enable error reporting when local variables aren’t read.
“noUnusedParameters”: true
Raise an error when a function parameter isn’t read.
“noImplicitReturns”: true,
Enable error reporting for codepaths that do not explicitly return in a function.
if the function doesn’t have any return, then it’s fine, however, if the function returns anything in any branching of condition, then it must return
in all branch.
JS Modules
ES feature support charte here
let
vs var
key difference - let
lets you define block scoped variable, which isn’t possible with var
.
function signature
const printOutput = (output: string | number) => console.log(output);
// with function signature of types of argument and return - we don't need to specify it in the function implementation
const printOutput1: (a: number | string) => void = (output) =>
console.log(output);
Spread operator
// Array spread
const hobbies = ["sports", "books"];
const activeHobbies = ["movies"];
const allHobbies = [...activeHobbies, ...hobbies];
activeHobbies.push(...hobbies);
// Object spread
const person = {
name: "Chandan",
age: 30,
};
// Following doesn't create a copy, instead it references to original person object.
const referenceCopiedPerson = person;
// Spread operator creates actual copy of person object
const copiedPerson = { ...person };
Using tuples for multi parameters, for same type of parameters with REST operator
// Accepts exactly three number parameters
const add = function (...numbers: [number, number, number]) {
return numbers.reduce((total, num) => total + num);
};
Array/Object destructuring
// Array destructuring
const hobbies = ["biking", "reading", "programming", "startup"];
const [hobby1, hobby2, ...remaining] = hobbies;
// Object destructuring
const person = {
firstName: "Chandan",
age: 37,
};
// here left side variable name must match the key of person object.
// If you want to assign it to another named variable, use "objet-property"`:` "variable-name"
const { firstName: userName, age } = person;
console.log(userName, age);
Class and Interface
A class can be faked by an object, having exact same set of attributes and methods.
private
and public
A method or property can be declared as private
by putting private
keyword in the beginning.
You can define the property as public
/private
in the class constructor. You can also define a property as readonly
constructor(private readonly id: string, public name: string){
// this.id = id; // Do not need this, as these are explicity defined in constructor arguments.
// this.name = name;
}
prototype
The javascript
prototype
property allows you to add new properties to object constructor.
function Person(first, last, age, eyecolor) {
this.firstName = first;
this.lastName = last;
}
Person.prototype.nationality = "English";
Person.prototype.name = function () {
return this.firstName + " " + this.lastName;
};
inhertiance and private
/protected
super()
must come before adding any new property to any subclass
class Department {
private employees: string[];
constructor(private id: string, public name: string){
}
}
class ITDepartment extends Department {
constructor(id: string, public admins: string[]){
super(id, 'IT'); // super must come before adding any new property to this subclass
this.admins = admins
}
addEmployee(name: string){
// this.employees.push(name) <-> It'll throw error, as `employees` is `private` property of Department class
}
}
const dept = new Department('dept_0', 'Electronics')
// dept.employees <-> Not accessible, as it's private property
const it = new ITDepartment('dep_1', ['Chandan'])
// it.employees.push('Darsh') <-> this will throw error, as `employees` is private property of `Department`
private
properties are available only inside the class defining it. No instance of the class or child class can access these private
properties directly. The defining class can refer to these private
properties inside the class definition, however the child class can’t refer to these private
properties even inside their class definition.
If you want to access a property, which is not open to use outside class definition, however accessible from the class and subclass, use protected
. This can be useful in overriding a function in sub-class if you want to use base class public/protected attributes.
getter
and setters
Getter/Setter can be used to get/set any type of property(public, private, protected) and perform any logic needed.
The methods defined with get
and set
can be directly accessed as property of object. No need to follow function call syntax.
Static methods and properties
static methods and properties are like class level property/variable and methods.
To access a static property(inside non-static method definition of class or outside), you use className.static_property
. To use static method, you use className.static_method(arguments)
. These static properties can’t be called on instance of class.
// example in existing libraries
Math.PI;
Math.pow(2,2);
// example
class Department {
static fiscalYear = 2023;
constructor(private id: string, public name: string){
console.log(Department.fiscalYear);
}
static createEmployee(name: string){
return {employee: name}
}
}
Abstract class
Abstract class can be defined with abstract
keyword. A method can be defined as abstract method with abstract
keyword. A class can have an abstract method if and only if, the class itself is an abstract class.
abstract class Department {
abstract describe(): void;
}
class ITDepartment extends Department {
describe(){
console.log("Defined abstract method in sub-class")
}
}
Singleton class
Singleton instance will have exactly one instance of a singleton class. This can be done by defining static instance
of type of class and a static method getInstance
to get the singleton object and marking the constructor private
class AccountingDepartment{
private static instance: AccountingDepartment;
private constructor(private id: string, public reports: string[]){
}
static getInstance(){
if(AccountingDepartment.instance){
return this.instance;
} else {
this.instance = new AccountingDepartment('id1', [])
return this.instance;
}
}
}
Interface
Interface describes a structure of object. It’s similar to custom type
. However, alias type
are mostly used with functions or define composite(union etc) types of base types (such as number, string etc). Interface is mostly used with objects.
interface Greetable {
name: string;
greet(text: string): void;
}
class Person implements Greetable {
age = 30;
constructor(public name: string){
}
greet(message: string){
console.log(message)
}
}
let user1: Greetable;
user1 = {
name: "Chandan",
greet(phrase: string) {
console.log(phrase, this.name)
}
}
user1.greet("Hi there, I'm")
interface
doesn’t define a method, however, abstract class can have some method implementation.
A class can implement
one or more interfaces, if so, then the class has to implement all the properties/methods of the interface.
interface
can have readonly
property definition, but it can’t have public
or private
. This readonly
property can be assigned only at the time of creation of object. The behaviour of this readonly
property can be changed in the class, which implements the interface.
An interface can extend another one or more interfaces. Multiple interfaces can be implemented in a class. A class can inherit from only one parent class, however, it can implment multiple interfaces.
inteface as function types
To define type
, we use =
, however, to define interface
, we use {}
.
Using type
for function types is probably is a better choice.
// type AddFunc = (a: number, b: number) => number;
interface AddFunc {
(a: number, b: number): number;
}
let add: AddFunc;
add = function (num1: number, num2: number) {
return num1 + num2;
};
A property or method can be marked as optional using ?
.
interface AnotherInterface {
optionalNickName?: string;
optionalMethod?: (text: string) => void;
}
NOTE: interface
is TypeScript feature to ease and structuring your code. There is no corresponding implementation in vanilla javascript.
Advanced types and Type Guards
interface Admin {
name: string;
priviledges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
interface ElevatedEmployee extends Admin, Employee {}
const e1: ElevatedEmployee = {
name: "Chandan",
priviledges: ["CEO", "CTO"],
startDate: new Date(),
};
class GreetEmployee implements ElevatedEmployee {
private static instance: GreetEmployee;
constructor(
public name: string,
public priviledges: string[],
public startDate: Date
) {}
static getInstance() {
if (GreetEmployee.instance) {
return this.instance;
} else {
this.instance = new GreetEmployee("Chandan", ["CEO", "CTO"], new Date());
return this.instance;
}
}
static greetEmployee() {
console.log("Welcome,", this.instance);
}
}
const e2: GreetEmployee = GreetEmployee.getInstance();
GreetEmployee.greetEmployee();
Intersection of object-types/interfaces creates a combination type, which implements all the properties of all the composing types/interfaces.
type Car = {
company: string,
model: number,
};
type Bike = {
company: string,
mileage: number,
};
// Intersection type
type Vehicle = Car & Bike;
const myVehicle: Vehicle = {
company: "Hyundai",
model: 2022,
mileage: 35,
};
// Union type
type UnknowVehicle = Car | Bike;
function vehicleInformation(vehicle: UnknowVehicle) {
console.log(`Your vehicle company is ${vehicle.company}`);
if ("mileage" in vehicle) {
console.log(`Your vehicle mileage is ${vehicle.mileage}`);
}
if ("model" in vehicle) {
console.log(`Your vehicle mileage is ${vehicle.model}`);
}
}
Intersection of literal types, creates an intersection of the composing types.
type Combinable = number | string;
type Numeric = number | boolean;
// Universal can only be 'number'
type Universal = Combinable & Numeric;
// const uni: Universal = true; // won't work
const uni: Universal = 1;
Github commit here
With type or interface, we can have union |
or intersection &
.
Union of two object types, creates a type having either of the object’s type
To check the type of object inside function of composite type (i.e. Union |
), you can use typeof
for objects based on inheritance/types. For class based objects, you can use instanceof
.
abstract class State {
public capital: string;
constructor(capital: string) {
this.capital = capital;
}
abstract getCapital(): string;
}
class Bihar extends State {
public language: string;
constructor(capital: string, language: string) {
super(capital);
this.language = language;
}
getCapital() {
return this.capital;
}
setLanguage(language: string) {
this.language = language;
}
getLanguage(): string {
return this.language;
}
}
class Jharkhand extends State {
public mines: string[];
constructor(capital: string, mines: string[]) {
super(capital);
this.mines = mines;
}
getCapital(): string {
return this.capital;
}
}
type IndianStates = Bihar | Jharkhand;
const state1 = new Bihar("Patna", "Hindi");
const state2 = new Jharkhand("Ranchi", ["Coal Mines"]);
function indianStates(state: IndianStates) {
console.log(state.getCapital());
if (state instanceof Bihar) {
console.log(state.language);
}
if (state instanceof Jharkhand) {
console.log(state.mines);
}
}
indianStates(state1);
indianStates(state2);
Discriminated Unions
In each object, keeping one common property, which describes the object. This property can be used to check the type of object to ensure type safety and to check which properties are available to object.
enum ANIMAL_TYPE {
BIRDIE = "birdie",
HORSIE = "horsie",
}
interface Bird {
type: ANIMAL_TYPE.BIRDIE;
flyingSpeed: number;
}
interface Horse {
type: ANIMAL_TYPE.HORSIE;
runningSpeed: number;
}
type Animal = Bird | Horse;
function movingSpeed(animal: Animal): void {
let speed;
switch (animal.type) {
case ANIMAL_TYPE.BIRDIE:
speed = animal.flyingSpeed;
break;
case ANIMAL_TYPE.HORSIE:
speed = animal.runningSpeed;
break;
}
console.log("Moving at speed ...", speed);
}
movingSpeed({ type: ANIMAL_TYPE.BIRDIE, flyingSpeed: 1000 });
movingSpeed({ type: ANIMAL_TYPE.HORSIE, runningSpeed: 100 });
When TS is unable to detect the actual type of an object, we can use Type Casting to let TS know the exact type of the object.
For example,
<p id="my-para"></p>
<input id="user-input" type="text" />
TS can’t define the precise type of the paragraph
/userInput
constant below. Thus we can do the typecasting.
const paragraph = document.querySelector("p")!;
const para = document.getElementById("my-para");
// One way of doing type casting. This type of syntax is also used in React
// const userInput = <HTMLInputElement>document.getElementById("user-input");
// Second way of Type casting
const userInput = document.getElementById("user-input") as HTMLInputElement;
userInput.value = "Please write a text here";
if (userInput) {
(userInput as HTMLInputElement).value = "Please write your text here";
}
Index types
We might want to data-type/interface, which can have any number of properties. For example an ErrorContainer
interface:
interface ErrorContainer {
// {email: "Invalid email", userName: "Invalid username."}
// id: number; // won't work, as we've defined [key: string]: string for all attributes
// [key: string]: string;
[prop: string]: string;
}
const err1: ErrorContainer = {
email: "Invalid email",
// more attributes where key is string and value is also a string...
userName: "No user found",
};
Function overloading
A function can be overloaded to define precise type of parameters and return values. Once function is overloaded, the return value can have behaviour of overloaded variable type if defined.
// function overloading
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string, b: number): string;
function add(a: number, b: string): string;
function add(a: Combinable, b: Combinable) {
if (typeof a === "string" || typeof b === "string") {
return a.toString() + b.toString();
}
return a + b;
}
// This add can call methods defined for strings
const sumResult = add("Chandan", "Jhunjhunwal");
sumResult.split(" ");
// This add can call methods defined for numbers
const sumResultNum = add(1, 7);
if (typeof sumResultNum === "number") {
console.log("I'm always true");
}
Optional Chaining
In case, we’re not sure of the property of the data (e.g. some data is retrieved from outside service), then we can use optional chaining to access the attributes of an object without any error (if it exists).
const fetchedData = {
name: "Chandan",
age: 37,
// job: {
// title: "CEO",
// company: "Own company!",
// },
};
// How we would do it, to ensure there is no error
console.log(fetchedData.job && fetchedData.job.title);
// or we can use `Optional chaining`
console.log(fetchedData?.job?.title);
Null coaleasing
To check assin a default only if a data is null
. If the data is not null, keep it as it is.
// Nullish coalesing
const userInputNull = null;
const userInputBlank = "";
// Prints DEFAULT
console.log(userInputNull ?? "DEFAULT");
// Prints ""
console.log(userInputBlank ?? "DEFAULT");
Generics - Handbook
Generics is used to describe the complete type of object such as Array or promises or any complex combination of object using custom types
const array: Array<string | number> = ["Chandan", 1231];
const first = { name: "Chandan" };
const age = { age: 37 };
function vanillaMergeObjects(obj1: Object, obj2: Object) {
return Object.assign(obj1, obj2);
}
const vanillaMergedObject = vanillaMergeObjects(first, age);
console.log(vanillaMergedObject);
// Even though the objects are merged, we can't access the property of objects directly
// console.log(vanillaMergedObject.name);
// console.log(vanillaMergedObject.age);
// The above problem is solved with Generics.
function mergeObject<T extends {}, U>(obj1: T, obj2: U) {
return Object.assign(obj1, obj2);
}
const mergedObj = mergeObject(first, age);
console.log(mergedObj.name);
console.log(mergedObj.age);
// we could also do like, but it's redundant.
const mergedObj1 = mergeObject<{ name: string }, { age: number }>(first, age);
A type check constraint can also be added in the generic type definition
// To ensure the correct objects are passed, instead of any in generic type T and U
function mergeObjectWithConstraint<T extends object, U extends object>(
obj1: T,
obj2: U
) {
return Object.assign(obj1, obj2);
}
// following will throw error due to type check for generic types T and U
// const mergedConstrainedObj = mergeObjectWithConstraint({ name: "Chandan" }, 30);
const mergedConstrainedObj = mergeObjectWithConstraint(
{ name: "Chandan" },
{ age: 30 }
);
We can also write function with generic types, which implements a/more particular method.
// We might want to write a function, which takes any object as argument,
// which responds to `length` method.
interface Lengthy {
length: number;
}
function printAndDescribe<T extends Lengthy>(element: T): [T, string] {
let descriptionText = "Got no value";
if (element.length === 1) {
descriptionText = "Got 1 value.";
} else if (element.length > 1) {
descriptionText = "Got " + element.length + " values.";
}
return [element, descriptionText];
}
console.log(printAndDescribe("Hello"));
console.log(printAndDescribe(""));
console.log(printAndDescribe([]));
console.log(printAndDescribe(["Chandan"]));
console.log(printAndDescribe(["Chandan", "Jhunjhunwal"]));
NOTE Usually if one parameter of a function is of generic type, then all the parameters are of generic type
To check the keyof
a generic type object, use keyof
// Check the keyof a generic type
function extractAndConvert<T extends object, U extends keyof T>(
obj: T,
key: U
) {
return "Value is: " + obj[key];
}
// console.log(extractAndConvert({}, 'name')) // throws error, no key 'name' in {}
// console.log(extractAndConvert({ name: "Chandan" }, "age")); // throws error, as no key 'age' in {name: "Chandan"}
console.log(extractAndConvert({ name: "Chandan" }, "name"));
Generic classes - Can be used to define a generic type of class
class DataStorage<T extends number | string | boolean> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
if (this.data.indexOf(item) === -1) {
return;
}
this.data.splice(this.data.indexOf(item), 1);
}
listItems() {
return [...this.data];
}
}
const stringStorage = new DataStorage<string>();
stringStorage.addItem("Chandan");
stringStorage.addItem("Jhunjhunwal");
stringStorage.addItem("test");
stringStorage.removeItem("test");
console.log(stringStorage.listItems());
Partial
Partial
allows us to define type
for an object even if a part of property is defined for the object.
interface Human {
name: string;
age: number;
weight: number;
}
// If person could be Partial of interface Human, Partial<T> can be used.
function partialDefinition<T extends Human>(person: Partial<T>) {
// let person: Partial<T> = {}
return person;
}
console.log(partialDefinition({ name: "Chandan" }));
function returnPartialObj(name: string, age: number, weight: number): Human {
let myHuman: Partial<Human> = {};
myHuman.name = name;
myHuman.age = age;
myHuman.weight = weight;
// myHuman.whatNot = 12;
return myHuman as Human;
}
console.log(returnPartialObj("Chandan", 21, 80));
Readonly generics type
// Readonly objects
const iAmReadOnly: Readonly<string[]> = ["Chandan", "Jhunjhunwal"];
// iAmReadOnly.push("Test");
console.log(iAmReadOnly);
Decorator
Decorator
is experimental feature of TypeScript. To enable decorator, update tsconfig.json
by uncommeting "experimentalDecorators": true
In the code below, the decorator Logger
accepts constructor
of the class Person
. The decorator gets executed, when the class Person
is defined in javascript file. It’s not executed, when the object of Person
is instantiated.
function Logger(constructor: Function) {
console.log("Logging...");
console.log(constructor); // it'll log the class
}
@Logger
class Person {
name = "Chandan";
constructor() {
console.log("Creating a new person...");
}
}
A decorator factory
can be used to customise the decorator, by returning a function, which takes constructor
as an argument.
// Decorator factory
function Logger(logString: string) {
return function (constructor: Function) {
console.log(logString);
console.log(constructor);
};
}
@Logger("LOGGING-PERSON")
class Person {
name = "Chandan";
constructor() {
console.log("Creating person....");
}
}
Decorator
can also be used for templating.
In case a class has more than one decorators, then Decorators
are invoked/executed from nearest decorator
to farther decorator defined on the class i.e. Bottom-UP fashion. However, the actual declaration of the decorators happen in the same order as it’s defined on the file.
@WithTemplate("<h1>My Person - Chandan</h1>", "app") // invoked after @Logger
@Logger("LOGGING-PERSON") // first invoked
class Person {
name = "Chandan";
constructor() {
console.log("Creating person....");
}
}
Default Decorator Arguments - to be passed to decorator’s returned function or to be defined as signature argument of decorator
Class - constructor
- These decorator can return override constructor, ie. modifying the behaviour of class
(instance/accessor)Methods - object prototype
+ name of method
+ method descriptor
- These decorator can change method/accessor, ie. modifying the behaviour of method/accessor
static methods - object constructor
+ name of method
+ method descriptor
property - object prototype
+ name of property
- These decorator’s returned value is ingored
argument decorator - object prototype
+ name of method having the argument
+ position
- These decorator’s returned value is ingored
If your decorator takes the above arguments in their signature, the decorator can be called without ()
, however, if you want to return a decorated function and have these arguments available to them, then you need to decorate with ()
function MethodDecorator() {
console.log("Decorating method...");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("//MethodDecorator - START");
console.log(target);
console.log(propertyKey);
console.log(descriptor);
console.log("//MethodDecorator - END");
};
}
function AnotherMethodDecorator(
target: any,
methodName: string,
descriptor: PropertyDescriptor
) {
console.log("//AnotherMethodDecorator - Without a return function - START");
console.log(target);
console.log(methodName);
console.log(descriptor);
console.log("//AnotherMethodDecorator - Without a return function - END");
}
@AnotherMethodDecorator // notice - NO `()` here
getAge() {
return this._age;
}
@MethodDecorator() // notice - required `()` here
draw() {
console.log("Drawing....");
}
Decorator executed at the time of defining the class. They don’t execute when a new instance of a the class is defined. Decorators meant for changing the behaviour of class/method/attributes at the time of definition to change their behaviour or do some behind the scene work or store some metadata etc. Decorator add some extra feature, which is tagged with the class/method/attributes declaration. However, decorator can also be written to do something at the run-time (i.e. when object is instantiated etc).
Autobind
Many times, when you want to call a function from another function, the definition of this
can change. For example:
class Person {
name = "Chandan";
showMessage() {
console.log(this.name);
}
}
const p = new Person();
const button = document.querySelector('button')!;
// button.addEventListener('click', p.showMessage) // won't work, as `this` will point to the `target` of `addEventListener`
button.addEventListener('click', p.showMessage.bind(p))
This can be achieved with decorator. Define a get
method in a modified method descriptor, which will get executed before the actual method execution.
function Autobind() {
return function (
_target: any,
_methodName: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const modifiedDiscriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
// it's like executing extra logic before the `value` is returned
const bindFn = originalMethod.bind(this);
return bindFn;
},
};
return modifiedDiscriptor;
};
}
class Legend {
name = "Chandan";
@Autobind()
showMessage() {
console.log(this.name);
}
}
const p = new Legend();
const button = document.querySelector("button")!;
// button.addEventListener("click", p.showMessage); // won't work, as `this` will point to the `target` of `addEventListener`
// button.addEventListener("click", p.showMessage.bind(p));
// Will start working with Autobind decorator, as `this` now correctly points to Legend object
button.addEventListener("click", p.showMessage);
Decorator can also be used to validate property of class, such as required, positive etc.
Class validator library - here
Typescript debugging in VS Code
https://code.visualstudio.com/docs/typescript/typescript-debugging
Gotchas | Tips & Tricks
- When adding
js
file reference in <head>, ensure you adddefer
to defer the load of script post document load.
<head>
<script src="app.js" defer></script>
</head>
- To get the class name of a given object, use
obj.__proto__.constructor.name
NOTE: Every javascript function hasname
key.
class Course {
title: string;
price: number;
constructor(t: string, p: number){
this.title = t;
this.price = p;
}
}
// Tested on browser console
$> const course = new Course('title', 10)
$> course.__proto__.constructor
$> course.__proto__.constructor.name
'Course'
$> course.__proto__
-
A)
const node = document.importNode(this.projectTemplate.content,true);
creates a few child and if you want to access html child, you’ll have to usenode.firstElementChild
and NOTnode.firstChild
``B) htmltemplate
doesn’t appear on page. It acts as a hidden from UI element. The template can be used to createHTMLFragments
, which can appended to any document’s node. userimportNode
to create imported node of the template. -
Three ways of binding
this
:
<button id="btn1">Test1</button>
<button id="btn2">Test2</button>
<button id="btn3">Test3</button>
<button id="btn4">Test4</button>
<script>
class Test1 {
constructor() {
btn1.addEventListener('click', this.logThis);
}
logThis() {
console.log(this); // button (since we didn't bind this)
}
}
class Test2 {
constructor() {
btn2.addEventListener('click', this.logThis.bind(this));
}
logThis() {
console.log(this); // class instance
}
}
class Test3 {
constructor() {
btn3.addEventListener('click', () => this.logThis());
}
logThis() {
console.log(this); // class instance
}
}
class Test4 {
constructor() {
btn4.addEventListener('click', this.logThis);
}
logThis = () => {
console.log(this); // class instance
}
}
const test1 = new Test1();
const test2 = new Test2();
const test3 = new Test3();
const test4 = new Test4();
</script>
TypeScript interative Shell
ts-node
can be used for TypeScript execution and REPL for Node.js
npm install -g ts-node
Then launch the REPL by ts-node
Copy of an array
const ar = [1, 2, 3];
const ar2 = [...ar];
const ar3 = ar.slice();
To clean up existing built files.
$> npm install --save-dev clean-webpack-plugin
TS Script useful utilities:
- https://github.com/typestack/class-transformer
- https://github.com/typestack/class-validator
- JavaScript to TypeScript types - https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types
- If a javascript library doesn’t have TS type definition, then you can use
declare
as last resort.
declare var GLOBAL: any;
TypeScript Promise
A promise need a Type
for resolve function. i.e. on success, it’ll return an object of type Type
. In case of failure (reject), since we don’t know, what will be returned, the reject’s return type is by default any
.
const myPromise =
new Promise() <
Type >
function (resolve, reject) {
// logic
};
// Alternatively
let myAnotherPromise: Promise<Type> = new Promise(function (
resolve,
reject
) {});
The promise function Object() accepts an executor callback, which the compiler will call with the following two arguments:
- resolve — This is a function for resolving the promise.
- reject — This is a function for rejecting the promise.
As a result, our commitment can either be fulfilled or denied. The resolve
part is handled by .then
, and the reject
part is handled by .catch
.
function types can be defined as:
const fetchHtmlPage: () => Promise<string> = () => {
// Function body
};
// OR alternatively
const fetchHtmlPage = (): => Promise<string> = () => {
// Function body
};
await
operator is used to wait for a Promise
. It can only be used inside an async function
within regular JavaScript code; however it can be used on its own with JavaScript modules.
TypeScript + React
- Install
create-react-app
package
$> npm install -g create-react-app
$> create-react-app <your-app-name> --template typescript
Read input field value
Using useRef
: a mutable reference object.
- First import
useRef
- Define a const/variable
textInputRef
usinguseRef
. InuseRef
also define the Type of HTML Element(which you want to reference to) and initial value - Define
ref
on theinput
element with a variable/const defined earlier - Now the value of user input can be read by
textInputRef.current.value
import React, { FormEvent, useRef } from "react";
const NewToDo: React.FC = () => {
const textInputRef = useRef<HTMLInputElement>(null)
function submitEventHandler(event: FormEvent) {
event.preventDefault()
const inputText = textInputRef.current!.value
console.log(inputText)
}
return <form onSubmit={submitEventHandler}>
<label htmlFor="new-todo">Todo Title</label>
<input type="text" id="new-todo" ref={textInputRef}/>
<button type="submit">Add To Do</button>
</form>
}
export default NewToDo
Defining props on a component
Let’s say we’ve a function myPersonalEventHandler
, which we want to pass to a component <MyPersonalComponent />
as props
<MyPersonalComponent onPersonalAction={myPersonalEventHandler}>
This onPersonalAction
must be defined in props
definition of MyPersonalComponent
component
inteface PersonalProps {
onPersonalAction: (text: string) => void;
}
const MyPersonalComponent: React.FC<PersonalProps> = (props) => {
props.onPersonalAction("It'll call function myPersonalEventHandler")
}
Notice, the onPersonalAction
is a prop, defined on the component, however the value inside {}
, i.e. myPersonalEventHandler
is available inside the original calling component. Here function myPersonalEventHandler
’s reference is passed to the component via onPersonalAction
prop.
Use state to manage inputs
import React, {useState} from 'react';
import TodoList from './components/ToDoList';
import NewToDo from './components/NewToDo';
import { ToDo } from './models/todo.model';
function App() {
const [todos, setToDo] = useState<ToDo[]>([])
const onNewToDo = (text: string) => {
setToDo([...todos, {id: Math.trunc(Math.random()*1000), name: text}])
console.log(todos)
}
return (
<div className="App">
<NewToDo onToDo={onNewToDo}/>
<TodoList items={todos}/>
</div>
);
}
export default App;
React might not update the todo
immediately, so using ...todos
might result in inconsistencies, so instead using the function signature of setState
.
const onNewToDo = (text: string) => {
setToDo((prevToDos) => [
...prevToDos,
{ id: Math.trunc(Math.random() * 1000), name: text },
]);
};
Delete a todo
You can add any number of props
along with the components, which are defined on the component.
In express, you get vanilla server. You can add middleware to get more functionalities. For example
- error middleware here
-
morgan
middleware for logging -
Router
middleware for handling CRUD requests with URL/todos
-
json
frombody-parser
to parse the body of request toJSON
```javascript import express, { Express, Request, Response, NextFunction } from “express”; import morgan from “morgan”; import { json } from “body-parser”; import { ToDoRouter } from “./routes/todos.routes”;
const app: Express = express(); const port: number = 3000;
// Middlewares app.use(morgan(“combined”)); app.use(json()); app.use(“/todos”, ToDoRouter); // Error Handling middleware app.use((err: Error, req: Request, res: Response, next: NextFunction) => { res.status(500).json({ message: err.message }); });
// app.request(err: Error, req: Request, res: Response, next: Next) app.listen(port);
```
This is a sapling 🌱 in my digital garden 🏡.