TypeScript - Part 1

Urvashi Patil-Sachdev
Full Stack Engineering
37 min readMay 10, 2022

Welcome to this series of TypeScript tutorials, tips and tricks. In these series of tutorials, we will go from fundamentals to advanced lessons on TypeScript, the concept and its applications.

What is TypeScript and Why do we need it?

TypeScript is an open-source programming language developed and maintained by Microsoft.

Before starting deep dive into TypeScript let's see why we need TypeScript. To understand this we need to know another programming language and its problems. And that language is JavaScript, the most loved and hated language on the planet.

We all know JavaScript is scripting as well as a programming language which runs either in the browser or by Nodejs. JavaScript is a dynamic and weakly typed language. This is an advantage and as well as cause some problems when developing complex applications.

Let's see problems in JavaScript as below.

  1. Variable declaration and assigning any type of value
nothing but JavaScript.var a; //Declare a variable "a"a=10;  //In JavaScript we can assign any type of value to same variable a="hello"; //stringa={}; //object

As we saw in Javascript once we declare a variable. It can hold any type of value at any time in execution. There is no strong type defined for a variable. This may cause errors at runtime. e.g. if the developer assumes that variable “a” is a number and applies a mathematical function on a value which is not a number then it will throw an error at runtime. This is because Javascript is a weakly typed language.

2. Function arguments

In javascript, there is no compulsion that we have to pass the same number of arguments to function when it was defined. We can pass any number of arguments more or less also allowed in javascript.

function(a,b){
return a+b;
}
add(1,2); //ans= 3 , passed 2 argumentsadd(1);//ans - NaN, passed 1 argument. b = undefinedadd(2,2,3); //ans- 4 , passed 3 arguments. Javascript ignored 3rd argument.

3. Object add/remove property at runtime

In Javascript, if we have created an object using a class or function constructor. We can add a new property at runtime even though that property does not exist while creating an object. Also, it is allowed to remove any existing property at run time. This may cause an accidental error if anyone is accessing removed property later in the program.

class Person{
constructor(fName, lName){
this.fName = fName;
this.lName = lName;
}
}
const p = new Person("Urvashi", "Sachdev");//allowed to add new property
p.address = "Mumbai";
//alllow to remove existing property
delete p.fName;

4. Multiple Browser consistency issues

We all know Browser understand only one language which is JavaScript. The browser has a JavaScript runtime environment to execute JavaScript code. Every browser has a JavaScript engine. e.g. IE has Chakra, Chrome has V8, etc.

Wach JavaScript Engine implements the ECMA standards set by Javascript. Since Javascript is evolving and new features are coming in every ECMA version. Each browser should adhere to ECMA standards to execute JavaScript code. But not all browsers implement at the same time. So it might be IE supports ECMA version 6 and but the browser does not support the same version yet. Due to this, if we use new JavaScript features in our program then there is no guarantee that our code will execute on all browsers. We need to handle each browser explicitly using polyfills.

To solve this issue we need some compiler or transpiler who transpile the code into the target JavaScript version so that it will get executed in all browsers without writing any browser-specific code.

So we can have any language + compilation = javascript code.

This “any language” is nothing but TypeScript.

TypeScript helps us to solve all the above issues as it is a strongly typed language. We can also say Types script is an extension of JavaScript (syntactic sugar over javascript)

To start with TypeScript basic requirement is we should know Javascript.

Typescript = Javascript + Type system + New javascript features

Browser and nodejs understand only javascript files. Then the question comes how do TypeScript files get executed?

TypeScript has a TypeScript compiler (tsc) which converts the TypeScript code into Javascript code and then gets executed by the browser or nodejs. Typescript files are denoted by extension .ts.

Note — Remember one thing TypeScript is active only during development. It does not provide any performance optimization.

Why TypeScript

  1. Help us to catch errors during development time
  2. TypeScript makes building complex applications more manageable
  3. Most of the frameworks use TS e.g. Angular (default using TS), React, Vuejs
  4. TS is a statically compiled language that provides optional static typing, classes, and an interface.
  5. TS is a strongly typed and compiled language, which may be closer to programmers of Java, C#, and other strongly typed languages, although the output of the compiler creates the same JS, which is then executed by the browser.
  6. Structural typing — Indispensable when you care about fully defining the actual structure you use. JavaScript allows for a lot of strange things to be done, so relying on a specific structure is a much safer solution.

TypeScript does not affect how our code gets executed by the browser or node. Writing TypeScript is the same as writing Javascript with some “extra documentation”.

Setup

We need to install 3 things.

  1. IDE — Integrated Development Environment — vscode
  2. nodejs
  3. TypeScript

IDE you can use any whichever you are comfortable with. I use vscode.

Nodejs — Install from nodejs site (https://nodejs.org/en/download/). Install based on your computer operating system. npm comes along with nodejs. This is used to install typescript.

Typescript — Install it using npm

npm install typescript -D 

We can also use Typescript REPL (Read Eval Print and Loop) to test typescript code online. https://www.typescriptlang.org/play

Anatomy of Typescript Project

In the Typescript project, there are the following types of files.

  • package.json — Package manifest — npm init -y
  • tsconfig.json — Typescript compiler settings — npx tsc --init
  • .ts → file contains both type information and code that runs
  • .js → file contains only code that runs
  • .d.ts → file contains only type information

Let's create a small Typescript program.

Create an indetx.ts file.

let message:string="Hello World";console.log(message);

Typescript can not be executed by nodejs or browser by itself. It should be compiled into .js and then can be executed in the browser.

Use Typescript compiler to transpile the code into Javascript.

npx tsc --watch

tsc- typescript compiler

watch- run compiler as soon as any change in typescript.

All tsconfig compiler options passed to tsc command as cli.

To execute the file

node lib/index.js

Target Compiler Option

In tsconfig.json we can define which version of javaScript code to be generated by the TYpeScript compiler. Check “compilerOptions” element.

{
"compilerOptions": {
"target": "es5", //specify ECMA script version default: //es3, es6, es2015 ,es2016,es2017,es2018,es2019

}

DataTypes in Javascript

A data type is a classification of data which tells the compiler or interpreter how the programmer intends to use the data. Most programming languages support various types of data, including integer, real, character or string, and Boolean.

Why do we need it?

  • Data types allow a compiler to allocate the right memory space for a variable and also let it perform type checking, making sure the program uses the variables correctly per their defined type.
  • Most server-side languages are statically Types
  • JavaScript is dynamically typed.

In Typescript every type can be annotated.

Type annotations + Type Inference

In Typescript, we can assign types to variables, functions and objects.

Type Annotations — Code we add to tell Typescript what type of value a variable will refer to. The developer tells the typescript the type.

Type annotation- colon and type separated by a space. (: number)

var myVal: string = "Hi all";

Type Inference — Typescript tries to figure out (or automatically figure out) what type of value a variable refers to. Typescript guesses the type.

Type annotation as shown below.

Let's see an example of how to annotate variables. Create a variable.ts file.

let isPreset: boolean = false; //boolean type variablelet magic: number = 66.6;let hello: string = "world";let notDefined: undefned = undefined;let notPresent: null = null;let penta: symbol = Symbol("star");

Type inference —If declaration and initialization are on the same line or at the same time, Typescript will figure out the type.

let isPreset = fale;  //booleanlet magic = 66.6; // numberlet hello = "world";  //stringlet notDefined = undefined;  //undefinedlet notPresent = null; //null is acting as a type and value both.let penta = Symbol("star");  //symbol  

In the above, we are not writing type annotation explicitly. Based on the value assigned while declaration typescript inferred it for us.

So then what is needed f Type annotation explicitly if typescript infers the type automatically?

We can do our programming without adding explicit type annotation and let Typescript infer/derive it. Majorly we use Type inference.

But we need explicit Type annotations in the following 3 scenarios→

  1. Delayed Initialization — When we declare a variable in one line and then initialize it later point in time.
  2. When we want a variable to have a type that CAN NOT be inferred correctly
  3. When the function returns ‘any’ type and we need to clarify the value.

Delayed Initialization — This happens many times we are going to get a value at a later point in time e.g. by service or from user input. In case we just declare a variable without type annotation then Typescript will infer it as “any”.

const words=["red","yellow","green"];let foundWord;  //ony declaration shows type 'any'//we can solve by adding type annotationslet foundWord: boolean;   // OR   let foundWord =false; //Type inferfor(let i=0;i<words.length;i++){       if(words[i] == "green"{            found=true;       }}

Type CAN NOT be inferred correctly

In the below example, numberAboveZero can be boolean or number

false if not found and NUMBER if found the number. In this case, type inferred will allow only 1 type as shown only boolean. Here Type inference can not work correctly. Ideally, this is bad code but sometimes we get some scenarios where more than 1 type needs to be set.

e.g. find a user from a list so it can be null or a user etc..

let numbers = [-1,10,-3];let numberAboveZero = false;for (let i=0; i<numbers.length; i++){     if(numbers[i] > 0){       numberAboveZero = numbers[i]; //Error as number can not assign to type boolean    }}

To solve this we can use Type annotation (union type). Union Type will see in detail later. At this point, union type tells us that variable numberAboveZero can hold either boolean or number type value.

let numberAboveZero: boolean | number;

When Function returns any

‘any’ — means Typescript has no idea what type of data is going to return.

Avoid type ‘any’ as much as possible. It is free from all TS checks. TS cant check for properties for correct references.

In Javascript we have a inbuilt function JSON.parse(). This function output value type depends upon the input given to this function. This type of function can not be inferred for the return value.

//because JSON.parse('4') return number//JSON.parse('true') return boolean//JSON.parse('{"value":5}') return object -->{value: number}//JSON.parse('{"name":"urvashi"}') return object -->{name: string}

Let's see the problem now.

const jsonObj='{"x":10,"y":20}';const coordinates = JSON.parse(jsonObj); //{x:10.y:20}//so now typescript will not give any error eventhough we are accessing "blhablah" property which is not there in jsonObj objectcoordinates.blhablah="asasa";  //So if type any then generally it is bad as TS can not do its job//coordinates is of type any inferred by Typescript

To fix ‘any’ we should add Type annotations.

const json='{"x":10,"y":20}';//Added type annotation for variable coordinates
const coordinates: {x: number, y: number} = JSON.parse(json);
//Now coordinates.blhablah="asasa"; will throw error earliet it was not due to type 'any'

Object Types and Type aliases

There are many ways in javascript to define an object like Object literal, using class, Object constructor etc.

Let's define an object using the object literal method.

//center object in Javascript 
let center ={
x:0,
y:0.
};
//center object in Typescript - Added type annotation
let center: {x:number, y:number} = {
x:0,
y:0.
}

Now if we want to create some more objects similar to the “center” object who has the same structure as x and y of number types. Then we will create it in typescript as below.

let leftPoint: {x:number, y:number} = {
x:0,
y:0.
};
let rightPoint: {x:number, y:number} = {
x:0,
y:0.
}

If you observe whenever e declare an object we define the same type annotation ( : {x:number, y:number} ) everywhere. Instead of defining the same type of object, again and again, we can create our custom type and use it.

Let's create a custom type. Below we have created a type Point whose structure is {x:number, y:number}.

type  Point = {x:number, y:number} ; 

This way we can use Point where we want objects of the same type/structure.

let center: Point = {
x:0,
y:0.
};
let leftPoint: Point = {
x:0,
y:0.
};
let rightPoint: Point = {
x:0,
y:0.
};

This way we create a Type alias (or Object type)for the object “center”. A type alias is nothing but giving a friendly name to our intent. Our intent here is to represent a point for the type. Defining a more meaningful name for the type, like semantic — the name of a thing describes the purpose.

Type alias advantages are as below

  1. Removes duplicate code and more readable and better code maintainability
  2. import and export these types and can be used across files.

The optional modifier “?”

Sometimes we have objects that have some optional properties.
In these cases, we need to identify the optional and the required properties.

To do this in typescript we have a special modifier named “optional” and it is identified by a question mark (?).

Let’s see an example

//Phone is not mandatory
type Person={
name: string,
email: String,
phone?:string
};
let bruce: Person = {
name:"Bruce",
email: "test@gmail.com"
};
let adam: Person = {
name:"Adam",
email: "test@gmail.com",
phone: 8712345678
}

In this example we can see the optional modifier in action, the “phone” property is marked as optional so in the “bruce” object we can avoid setting the “phone” property. By default, if nothing is set then in, Javascript assumes the value is undefined.

So can we say type Person as below?

type Person={
name: string,
email: String,
phone: string | undefined
};

Above “phone” is either string or undefined. As javascript takes phone value as undefined if nothing is set then this should also work correctly. But the type below even though “phone” is allowed to take undefined, “phone” is a mandatory property and not optional. So to make a property optional we should use the optional modifier “?” which allows us to omit the property.

Note — So optional “?” is more than just adding “string | undefined”

Array

Arrays are a neat way to store continuous items in memory in a single variable. You can access elements in the array by their index. The index of the array starts with 0.

JavaScript arrays can contain any combination of types and have variable lengths, which means that values can be added and removed dynamically without the need to explicitly resize the array.

TypeScript doesn’t change the flexible sizing of arrays, but it does allow the data types they contain to be restricted through the use of type annotations.

//Empty Array
let arrayNum: number[] = [];
let array =[1,2,3];
//Typescript infers type during initialisation as number[]
let arrayNum = [1,2,3];
//Arrays can be of any length
arrayNum=[1];
arrayNum=[1,2,3,4];
arrayNum= ["hello"]; //Error - Type 'string' is not assignable to type 'number'.

In javascript in ES6, there is a new operator introduced called spread operator — 3 dots (…). It is used to spread the values of the array as comma-separated.

Example as below

The spread operator (…) can also use for objects to spread the object’s key-value pair as comma-separated.

The spread operator works the same way in Typescript also. I have given you an overview as we will be going to use this operator quite often.

Tuple

A tuple is nothing but a fixed-length, ordered array.

Example

//statusTuple is a tuple with 2 elements of type number.
let statusTuple: [number,number];

statusTuple= [1,2];
statusTuple= [20,123];
statusTuple = [5]; //Error: must be 2 items
statusTuple = [5,6,7]; //Error: must be 2 items

We can also define a tuple with each element of different types.

//Different types in tuples
let hat: [string, boolean] = ["Hat", true];
let gloves: [string, number] = ["Gloves", 75];

Now make a note the order in the tuple is important. We can not assign number first and string second in “gloves”.

//Error - Type 'number' is not assignable to type 'string'
let gloves: [string, number] = [75 ,"Gloves"]

Tuple with names

Now let’s create a tuple for an address.

type Address = [number, string, string, number];

We wanted to create a function to print the address and the parameters to this function are comma-separated address details. To convert the array into a comma-separated let's use the rest parameter.

function printAddress(...address: Address){
}
printAddress() //on over --> printAddress(address_0: number, address_1: string, address_2: string, address_3: number): void

While sending the address to function printAddress(), we won't come to know what is 1st number means and what the 4th number means. What values should be sent? If we give some meaningful names to tuple elements then it will be more readable.

type Address = [streetNumber: number, city: string, state: string, postal: number];printAddress() //on over --> printAddress(streetNumber: number, city: string, state: string, postal: number): void

As we see in the above example we have given a meaningful name to each tuple element. Now we can find out that 1st number is street number and 4th number is postal code.

Remember one thing with a tuple, methods of arrays don’t have those checks.
Push and pop allowed on tuples.

TS does not stop us to do push and pop on tuple. Because there is no type equivalence check on num pair that happens when we invoke methods on it. There is no concept like this in the language yet.
So you have to be aware that tuples only get validated on assignments.

//tuple of number and string
const price: [number, string] = [123,"price"];
price.push(11); //Allowed --> [123, "price", 11] --> 11 added
price.push(221); //Allowed --> [123, "price", 11, 221] --221 added
price.pop(); //Allowed --> [123, "price", 11] --> 221 removed

Using Tuple Types

Tuples have a distinct type that can be used just like any type, which means you can create arrays of tuples, use tuples in type unions, and use type guards to narrow values to specific tuple types,

let hat: [string, number] = ["Hat", 100];
let gloves: [string, number] = ["Gloves", 75];
//Let's create products of type of tuple array
let products: [string, number][] = [["Hat", 100], ["Gloves", 75]];
//Tuple with type unions
let tupleUnion: ([string, number] | boolean)[] = [true, false, hat, ...products];

Tuples with Optional Elements (?)

Tuples can contain optional elements, which are denoted by the question mark (the ? character). The tuple is still fixed-length, and the optional element will be undefined if no value has been defined.

Optional parameters are defined by placing a question mark after the parameter name.

let hat: [string, number, number?] = ["Hat", 100]

Can we define a tuple with variable length? As per the definition, it is a fixed-length array. But yes using the rest parameter (…) we can create a tuple with variable length.

Let’s assume we wanted to create a sandwich order with order value, type of sandwich and toppings. Toppings can be any. It can be 1 or more than one. It is variable based on user liking.

Let’s create a type for SandwithOrder which is a tuple with order value, type and toppings.

type SandwitchOrder=[
number, //Order value
Sandwitch, // "Hamburger"
...string[] //toppings -variable use rest parameter
];

Important Note

  1. In tuple Order is important. We have to assign the data in the same order.
  2. We generally used tuples when we want to return multiple values from a function. e.g. React hooks (useState), etc..

Functions

We all know the function is nothing but a block of code to perform a specific task and can be executed independently. It is used to write reusable code.

In typescript, we apply type annotation in functions for

  1. Type Annotations to Function Parameters and Return Value. We annotate what goes inside the function (parameters) and what comes out from the function (return value).
  2. Type annotation to function as signature

Let’s see an example of functions in typescript.

function add(a: any,b: any){
return a+b
}
let result = add(1,2); //resut: any -->type infer
//It is always better to give proper type anotation
function add (a: number, b: number): number{
return a +b;
}
console.log("Add",add(1,2)); //3
console.log(add(2,3)); //

In the above example function add has 2 parameters “a” and “b” and both are of type “number”. This function returns a number.

We can also add a type annotation to the function signature as below. It is a little bit of nasty syntax.

//In javascript function
const logNumber = (i)=>{
console.log(i);
}
//In typescript
const logNumber: (i: number)=> void = (i: number)=>{
console.log(i);
}

In the above example type annotation starts after the colon (:) — : (i: number)=> void

We can also create a type alias for a function as below. So that we can reuse type alias at many places.

//Type alias
type LogNumber = (i: number)=> void;
const logNumber: LogNumber = (i: number)=>{
console.log(i);
}

void

Void is a special type, that’s specifically used to describe function return values. The return value of a void function is intended to be ignored.

Sometimes function does not return anything. In javascript, if the function does not return anything then by default “undefined” is returned.

Void can only be used as a return type as void means the return value to be ignored or left out.

We could type functions as returning undefined, but some interesting differences highlight the reason for the void’s existence:

function invokeInFourSecondsRetUndefined(callback: () => undefined) {
setTimeout(callback, 4000)
}
function invokeInFiveSecondsRetVoid(callback: () => void) {
setTimeout(callback, 5000)
}

const values: number[] = [];
//ERROR - Type 'number' is not assignable to type 'undefined'.
invokeInFourSecondsRetUndefined(() => values.push(4));
//Here even though array.push() returns a number it will get ignored as return type marked as "void"
invokeInFiveSecondsRetVoid(() => values.push(4));

Function Parameters

Function parameters are mandatory by default, but this can be changed by using optional parameters.

Optional parameters are defined by placing a question mark (?) after the parameter name.

Note — Optional parameters must be defined after the required parameters. This means that I cannot reverse the order of the amount and discount parameters.

For example, “amount “ is required and a “discount” is optional.

function calculateTax(amount, discount?) { 
return (amount * 1.2) - (discount || 0);
}
let taxValue = calculateTax(100, 0);
console.log(`2 args: ${taxValue}`); //-->2 args: 120
taxValue = calculateTax(100); // discount not passed
console.log(`1 arg: ${taxValue}`); //-->"1 arg: 120"

Callers of the calculateTax function can omit a value for the discount parameter, which will provide the function with an undefined value parameter. Functions that declare optional parameters must ensure they can operate when values are not supplied and use the logical OR operator (||) to coalesce undefined values to zero if the discount parameter is undefined, like this.

Parameter with a default value

Instead of an optional parameter, we can also define a parameter with a default value. In this case, if the argument is not passed then the default value will be taken into consideration for that parameter.

function calculateTax(amount, discount=0) { 
return (amount * 1.2) - discount;
}
let taxValue = calculateTax(100);
console.log(`2 args: ${taxValue}`); //-->2 args: 120
function calculateTax(amount, discount=0) {
return (amount * 1.2) - discount;
}
let taxValue = calculateTax(100);
console.log(`2 args: ${taxValue}`); //-->2 args: 120

Function Overloading

We all know what is function overloading. Function with different signatures. Different input parameters and return values.

In typescript, we need to create different signatures also known as heads for all overloads. But the implementation of all overloads will be there in one function.

Overloading function types

Typescript does NOT support the function overloading as supported by languages such as C# and Java.

Only the type information is overloaded by this feature for type checking. As shown below, there is only one implementation of the function, which is still responsible for dealing with all the types used in the overload.

//type overload signature
function calculateTax(amount: number): number;
//type overload signature
function calculateTax(amount: null): null;
function calculateTax(amount: number | null): number | null {
if (amount != null) {
return amount * 1.2;
} return null;
}

Each type of overload defines a combination of types supported by the function, describing the mapping between the parameters and the result they produce.

Function signature explained as shown below:

In the below example, there are 2 function overloads of testDate(). But actual implementation in 3rd common function handling all 2 overload cases.
Note — you are only able to call into the two “heads”, leaving the underlying “third head + implementation” inaccessible from the outside world.

//first overload
function testDate(a: string): string;
//second overload
function testDate(a: string, b:string, c: string): string;
//implementation
function testDate(a: string, b?:string, c?: string): string{
if(b != null && c!=null){
return b + c;
}
else{
return a;
}
};
testDate("111");
testDate("111","222"); //Error- No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments

noImplicitReturn compiler option

When no return statement in the javascript function then by default “undefined” gets returned.

In typescript, if we enable (tsconfig.json), TypeScript will check all code paths in a function to ensure they return a value.

//Error- Not all code paths return a value.
function toggleCoin(status: string){
if( status == "heads"){
return "tail";
} else {
"heads";
}
}

IN the above example there is no return value in the else condition. If we enable noImplicitReturn flag then all code paths must return a value explicitly.

It is better we set noImplicitReturn true which will help us to know if any code path does not return the value and we can avoid accidental runtime errors.

strictNullChecks compiler option

When strictNullChecks is false, null and undefined are effectively ignored by the language. This can lead to unexpected errors at runtime.

When strictNullChecks is true, null and undefined have their distinct types and you’ll get a type error if you try to use them where a concrete value is expected.

Let’s take an example. We have a function print() of type wither {age: number} or null. In that case user.age will throw an error if a user is null. If we set strictNullChecks true then the compiler will show the error at compile time itself as “Object is possibly ‘null’.”

If we set strictNullChecks false then compiler will not show the error at compile tie. The error will be thrown at runtime. It is best practice to enable strictNullChecks.

function print(user: {age: number} | null){
console.log(user.age); //Error - Object is possibly 'null'.
}
print(null)

The following ways we can solve problems in typescript for functions.

Typescript types

Union Types

TypeScript’s type system allows us to compile more than one type. A union type is a type formed from two or more other types, representing values that may be any one of those types. We refer to each of these types as the union’s members.

Union Types

  • Similar to “union” types in C/C++.
  • Done at the declaration and not a new type
  • Uses vertical pipe “ | “ character to donate “or”
  • anything assignable to one of the union’s members is acceptable
  • any property from any of the union’s member is assignable.
var myData: string | number ="Hi all";myData= 10;myData= true; //error- Type 'boolean' is not assignable to type 'string | number'

In the example above, “myData" variable can hold a value of either type string or boolean.

Let’s create a function flipCoin() which returns “heads” or “tails”. flipCoin() either return “heads” or “tails” based on logic. IN this case, return value of the function is either “heads” or “tails”. So we can write it as union “heads” | “tails”.

function flipcoin(): "heads" | "tails"{
if(Math.random() > 0,5) return "heads";
return "tails";
}
const result = flipcoin(); //"heads" | "tails"

Any and unknown (Top types)

TYpescript support 2 top types — any and unknown. They are called Top types because can hold any value (anything).

A top type (symbol: ⊤) is a type that describes any possible value allowed by Javascript. TypeScript provides two of these types: any and unknown.

Both are known as Universal types in Typescript. Any and unknown both accept any data.

Any Type

Let’s define a variable “exampleAny” of type “any”.

let exampleAny: any;//any -- Accepts all type data
exampleAny=123; // accepts number
exampleAny= "Hello"; // accepts string
exampleAny= window.document; // accepts object
exampleAny= setTimeout; // accepts function

If we call any method on “exampleAny” which does not exist. Then also typescript compiler will not show any error/warning. But at runtime error will be thrown.

We can call any method on “any” type variable in spite of whether it exists or not.

exampleAny.allows.anything.imagein(); //No compile time error even though imagine() not exists. exampleAny does not have any property "allows"

Typescript allows this because “any” is completely free from Typescript. It bypasses the Typescript checks. “any” is the most flexible type just like the JavaScript variable. We can also call “any” a wild card. We should use “any” carefully.

Sometimes it’s exactly the right type to use for a particular situation.

For example, `console.log`:

console.log(window, Promise, setTimeout, "foo");

//(method) Console.log(...data: any[]): void

We can see here that `any` is not always a “bug” or a “problem” — it just indicates maximal flexibility and the absence of type checking validation.

Unknown Type

Unknown accepts any type of data. But we can not use it freely. This is more typeSafe than Any.

The unknown can assign to only the unknown or use an extra type check to the respective type.

Let’s define a variable “exampleUnknown” of type “unknown”.

let exampleUnknown: unknown;//unknown-- Accepts all type data
exampleUnknown=123; // accepts number
exampleUnknown= "Hello"; // accepts string
exampleUnknown= window.document; // accepts object
exampleUnknown= setTimeout; // accepts function

But if we can call any method on an “unknown” type variable Then it shows an error because values with an unknown type cannot be used without first applying a type guard. Before calling any method in the unknown we need to check if the unknown variable has that method or not.

myUnknown.it.is.possible.to.access.any.deep.property  //Error - Object is of type 'unknown'.

If we want to use an unknown type variable then we can add a type guard check as below.

exampleUnknown.trim(); //Error as trim() is applicable to only string type//To make it work we need to add Type guard or check of type
if(typeof exampleUnknown == "string"){
exampleUnknown.trim(); //NO Error
}
//Can not assign Unknown to any type without type check
if(typeof exampleUnknown == "boolean"){
let unknownSetBool: Boolean = exampleUnknown;
}

If we wanted to assign an unknown type variable to a string then we have to check the type of unknown variable is a string or not. Here I have used the ternary operator (?:)

let exampleUnknown: unknown = "Hi";let myData: string = (typeof exampleUnknown === "string" )? exampleUnknown : "";

Practical use of top types

You will run into places where top types come in handy very often. In particular, if you ever convert a project from JavaScript to TypeScript, it’s very convenient to be able to incrementally add increasingly strong types. A lot of things will be `any` until you get a chance to give them some attention.

`unknown` is great for values received at runtime (e.g., your data layer).
By obligating consumers of these values to perform some light validation before using them, errors are caught earlier, and can often be surfaced with more context.

Generally, we should annotate errors as unknown.

In Javascript, we can throw the errors in 2 ways.

throw new Error("Error ocuured"); //Stacktrace existthrow "Foo"; //no staktrace

In the first way using new Error(), the error object consists of the stacktrace. But in a 2nd way, the error object does not contain “stacktrace”. So if any developer throws an error differently and we are accessing err.stacktrace then it might throw an error in 2nd approach. That's why it is recommended to use the “unknown” type of error.

If we use the “unknown” type then to use the unknown type “err” object we need to add a type guard to check whether “err” is of type “Error” or not. Since it is an instance variable so we need to add type guard using “instanceof”.

try{
}
catch(err: unknown){
if(err instanceof Error) throw err; else throw new Error(`${err}`);
}

There is also a “useUnknownInCatchVariables” compiler option that will make thrown values unknown across your entire project.

Literal Type

Using literal types you can allow an exact value that a string, number, or boolean must have.

Let’s define a variable “direction”.

let direction: string;

Since “direction” is of type string means we can assign any string value to it.

direction= "North";direction = "N0r7h";

Both above statements are valid. But what if you want “direction” must contain a valid direction name (East/West/North/South). Other than nothing should be allowed. In that case, we can define it as a literal type where we can mention the exact values allowed. Let's define “Direction” as a type so that we can use it wherever we want to use direction.

type Direction = "North" | "South" | "East" | "West";function move(distance: number, direction: Direction): void {
console.log(`Moving ${distance} meters from ${direction}`);
}
move(10, "East");

If we type the wrong direction then the compiler will show an error.

move(10, "EastWest"); //Compile time error -//Argument of type '"EastWest"' is not assignable to parameter of type 'Direction'.

One more place where typeScript used literal type. When we define a constant variable, then the value is treated as a Literal type.

const age = 6;  //compiler shows--> const age: 6

In the above case, TypseScript does not infer the “age” variable of type “number”. Since “const” is used, it is treated as a literal type where const age can only hold value 6.

Remember one when if the same variable “age” is defined as “let” then it won't be considered as literal type. ”let” can change, and so the compiler declares it a number.

let age = 6; // Compiler shows--> let age: number

Enums

Enum is a way of giving more friendly names to sets of numeric values.

In languages like C# /Java, an enum is a way of giving more friendly names to sets of numeric values.

Enums are not supported in JavaScript natively. TypeScript added Enum type.

  • Enum represents a bound set of possible value
  • Enum usually starts with 0 and increments if not specified
  • Enum backed by numeric value

An enum is defined using the enum keyword, followed by a name, followed by a list of values in curly braces as shown below.

Each enum value has a corresponding number value that is assigned automatically by the compiler and that starts at zero by default.

This means that the numbers used for the Bat, Ball, and Gloves names for the Product enum are 0, 1, and 2,

enum product  {
"p1",
"p2"
};
console.log("access",product.p1); // Result - 0console.log("by index",product[0]); // Result - "p1"

By default, enums begin numbering their members starting at `0`. You can change this by manually setting the value of one of its members. For example, we can start the below example at `1` instead of `0`:

enum Color {
Red = 1,
Green = 2,
Blue = 4,
}

let c: Color = Color.Green;
console.log(c); //Result - 2

Or, even manually set all the values in the enum:

enum Color {
Red = 1,
Green = 5,
Blue = 8,
}
let c: Color = Color.Green; //Result --> 5

String Enums

The default implementation of enums represents each value with a number, but the compiler can also use string values for enums. An enum can contain both string and number values, although this is not a feature that is widely used.

enum City { London = "London", Paris = "Paris", NY = "New York"};console.log(`City: ${City.London}`); //Result --> City: London

Some more examples of Enums are as below.

enum OtherEnum { First = 10, Two = 20 }enum Product { Hat = OtherEnum.First + 1 , Gloves = 20, Umbrella = Hat + Gloves }

let productValue: Product = 11;
let productName: string = Product[productValue];

console.log(`Value: ${productValue}, Name: ${productName}`); //Result - Value: 11, Name: Hat

Const Enums

1. Const enums are compile-time computed for efficiency
2. Const enums do not allow to access the data using values.

const enum Product { Hat, Gloves, Umbrella};let productValue = Product.Hat; let productName = Product[0]; //Error - A const enum member can only be accessed using //a string literal

The object used to represent a normal enum is responsible for providing the lookup feature and isn’t available for const enums.

const enums provide a way to reduce the overall memory footprint of your application at runtime by emitting the enum value instead of a reference.

The correct way to access in case of const enum as below.

let productName = Product.Hat;  

Tuple with Enums

We can define tuples using enums as datatype of each element.

Let’s take the same sandwich example of tuple and add enum for sandwich type.

enum Sandwich{ 
Hamberger,
VeggeBurger,
GrilledCheese
};
//tuple
type SandwichOrder=[
number, //Order value
Sandwich, // type
...string[] //toppings
];
const order1: SandwichOrder=[15, Sandwitch.Hamberger,"cheese"];const order2: SandwichOrder=[12.89, Sandwitch.GrilledCheese,"cheese","Olive"];

Classes

In JavaScript, classes are implemented using prototypes. The prototype is used to share the data across objects.

In JavaScript, we define a class as below.

class Vehicle{      
drive(){
console.log(`Driving a vehicle`);
}
honk(){
console.log("Beep beep");
}
}
let vehicle= new Vehicle();
vehicle.drive(); //Instance methods
vehicle.honk(); //Instance methods

In TypeScript, we define class as below. The added return type for methods (void).

class Vehicle{      
drive(): void{
console.log(`Driving a vehicle`);
}
honk(): void{
console.log("Beep beep");
}
}
let vehicle= new Vehicle();
vehicle.drive(); //Instance methods
vehicle.honk(); //Instance methods

In TypeScript, access modifiers are added.

JavaScript doesn’t provide access controls, which means that all of an object’s instance properties are accessible.

TypeScript treats properties as public by default when no keyword is specified, although you can explicitly apply the public keyword to make the purpose of the code easier to understand.

Let’s create an Employee class with public and private properties.

class Employee {
public id: string;
public name: string;
private dept: string;
public city: string;
constructor(id: string, name: string, dept: string, city: string) {
this.id = id;
this.name = name;
this.dept = dept;
this.city = city;
}
writeDept() { //default public
console.log(`${this.name} works in ${this.dept}`);
}
}
let salesEmployee = new Employee("fvega", "Fidel Vega", "Sales", "Paris"); console.log(`Dept value: ${salesEmployee.dept}`) //Error -Property 'dept' is private and only accessible within class 'Employee'

The access protection features are enforced by the TypeScript compiler and are not part of the JavaScript code that the compiler generates. Do not rely on the private or protected keyword to shield sensitive data because it will be accessible to the rest of the application at runtime.

JavaScript Private fields

TypeScript supports a JavaScript feature working its way through the standardization process and that is likely to be added to the language specification. This feature supports private fields, which provides an alternative to the private keyword. Private fields are denoted with the # character.

The key advantage over the TypeScript private keyword is that the # character is not removed during the compilation process, which means that access control is enforced by the JavaScript runtime.

  1. #name: string — It is a JS private field. It is actually inaccessible outside of a class at runtime.
  2. private name: string — It is a Typescript private field. While type checking it is inaccessible outside of a class. But at runtime, it is accessible outside of a class at runtime.
//Define private variables using #
class Animal{
#name: string; //(private in Typecript and Javascript both)
constructor(name: string){
this.#name = name;
}
move(distanceinMeters: number){
console.log(`${this.#name} is moved by ${distanceinMeters}`)
}
}let animal= new Animal("Cat");console.log(animal.#name); //Error - Property '#name' is not accessible outside class 'Animal' because it has a private identifier.

Shortcut to create instance properties

Let’s define “Person” class with 2 instance properties (name and age). We will first define at the class level with access modifiers and the type of each. Then we will define variables of the same type for each instance property in the constructor. In Constructor, we initialized the values to instance properties.

Using the shortcut method we don't need to define it twice. We just have to define once in the constructor function using access modifiers.

class Person{

constructor(public name: string, public age: number){
this.name = name;
this.age = age;
}
}

The compiler automatically creates an instance property for each of the constructor arguments to which an access control keyword has been applied and assigns the parameter value. The use of the access control keywords doesn’t change the way the constructor is invoked and is required only to tell the compiler that corresponding instance variables are required.

Ensuring instance properties are initialized

Let’s defined a class “Employee” with instance properties id, name and city. If any of the instance properties is not initialized then it may cause an error while accessing it and applying operations on it. If we want that all instance properties to be initialized then we need to use “strictPropertyInitialization” compiler option.

When the strictPropertyInitialization configuration option is set to true, the TypeScript compiler reports an error if a class defines a property that is not assigned a value, either as it is defined or by the constructor. The strictNullChecks option must also be enabled for this feature to work.

class Employee {
public id: string;
public name: string;
public city: string; //Error - Property 'city' has no initializer // and is not definitely assigned in the // constructor
constructor(id: string, name: string, dept: string, city: string) {
this.id = id;
this.name = name;
}
writeDept() { //default public
console.log(`${this.name} works in ${this.city}`);
}
}

Class property inference from the constructor

If we define the instance properties without a type annotation, and then in the constructor, we initialized instance properties. In this case, TypeScript inferred the property types based on the initialized value in the constructor.

This only works when “noImplicitAny ” is set to true in tsconfig.json.

Let’s define a class Color with 2 instance properties (red and green). Here we do not type annotating it while defining. Ideally, it should be inferred as “any” by the TypeScript compiler.

But since we are initializing values in the constructor, based on initialized values Typescript will infer it correctly.

class Color{      
red;
green;
constructor(c: [number,number]){
this.red=c[0];
this.green = c[1]
}
}

If any type than simple primitive then better to use type annotation explicitly rather than depending on the TypeScript compiler to infer. It is to better use type annotation explicitly always.

Inheritance

In Typescript, we can implement inheritance by using the “extends” keyword.

Using the “extends” keyword, TypeScript requires that the superclass constructor is invoked using the super keyword, ensuring that its properties are initialized.

Example of inheritance in Typescript as below.

class Vehicle{      
drive(): void{
console.log(`Driving a vehicle`);
}
honk(): void{
console.log("Beep beep");
}
}
class Car extends Vehicle {
//overrding methods
drive(): void {
console.log(`Driving a car`);
}
}
let car= new Car();
car.drive(); //Instance methods // Result- Driving a car

Abstract Class

  1. An abstraction is a way of hiding the implementation details and showing only the functionality to the users. In other words, it ignores the irrelevant details and shows only the required ones.
  2. We cannot create an instance of Abstract Class.
  3. It reduces the duplication of code.

Let’s define an abstract class “Command”.

abstract class Command{       abstract commandLine(): string;        execute(){            console.log("Executing" + this.commandLine());        }}

An abstract class is used only for inheritance. In TypeScript “extends” keyword is used for inheritance. Also, abstract methods have to be implemented in child classes.

In the above case, all child classes inheriting from the “Command” class must implement “commandLine()” function.

class GitRestCommand extends Command {
commandLine(){
return "git reset --hard"
}
}
class GitFetchCOmmand extends Command {
commandLine(){
return "git fetch --all";
}
}

Interfaces

What is an interface in OOPs?

Interfaces allow you to specify what methods a class should implement. Interfaces make it easy to use a variety of different classes in the same way.

In TypeScript, we can say

  1. Interfaces + Classes = How we get really strong code reuse in TypeScript.

2. Interface creates a new type, describing the property names and value types of an object

3. Interface is used to define an object type. It s used to define the contracts.

4. Usage of Interface is similar to Type aliases. Classes and Interfaces are both for type safety.

We can define the interface using the “interface” keyword in TypeScript.

interface Vehicle {
name: string;
year: number;
isNew: boolean
}

Let’s create a function printVehicle() which accepts the argument of type Vehicle. Here we are using “interface” as a type annotation.

function printVehicle(vehicle: Vehicle): void{
console.log(`
Name: ${vehicle.name}
year: ${vehicle.year}
isNew: ${vehicle.isNew}
`);
}

Now when we call the function printVehicle(), we must pass an object which adheres to the type “Vehicle”. This means the input argument should have the same structure as “Vehicle”.

const fourWheeler = {
name: "Honda City",
year: 2001,
isNew: true
}
printVehicle(fourWheeler);

We can also use any datatype, custom or instance type or functions in interfaces.

E.g. Let’s add the year of type Date and Summary as function in the interface.

interface Vehicle{
name: string;
year: Date;
isNew: boolean,
summary(): string
}
const fourWheeler = {
name: "Honda City",
year: new Date(),
isNew: true,
summary(){
return `Name is ${this.name}`
}

}
function printVehicle(vehicle: Vehicle): void{
console.log(vehicle.summary());
}
printVehicle(fourWheeler);

Advantages using interfaces
- Reusable code
- Developers coming from diff programming lang. It's similar and easy for them
- Interface Declaration merging (Not supported by Type aliases)

Interface Declaration merging

In TypeScript, multiple interfaces declaration with the same name is allowed. All same name declarations will get merged to create a single interface.

At the most basic level, the merge mechanically joins the members of both declarations into a single interface with the same name.

Let’s define 2 interfaces with the same name “Point2D”. 1st declaration has 2 properties (x: number and y: number ). 2nd declaration has 1 property (z: number). Due to declaration merging, it will merge into 1 as “Point2D” having all 3 properties (x: number, y: number and z: number).

interface Point2D{
x: number,
y: number
};
interface Point2D{
z: number,
}
let p: Point2D ; //--> {x: number, y: number, z: number }

It is used to add additional properties to the predefined interface.

If you are working with any API which needs seamless structural extension then we need to use this.

In express.js 3rd party, the library has a “Request” interface defined as per their implementation. While using in our project if we want to add our own property e.g. “json” then we can add it by declaring interact with the same name “Request” as shown below. The newly added property will be available throughout our application.

//E.g express request in node jsinterface Request{
body: any
}
//This is adding json property to existing Request interface
interface Request{
json: any
}
function handleRequest(req: Request){
console.log(req.body, req.json);
}

Where to use Interface

Sometimes we wanted to add the things to global objects.
Imagine a situation where we want to add a global property to the window object.

window.document //an existing propertywindow.myNewProperty =42;//tells TS compiler that "myNewProperty" exists
interface Window {
myNewProperty: number
}

Using interface we are just augmenting Window.

Type alias and Interfaces

Similarity

  1. Both are used to create a new type, describing the property names and value types of an object
  2. Both for Type safety
  3. Both can be implemented using the “implements” keyword

Let’s define type “Animal” and interface “MyPet” both can be implemented using the “implements” keyword as shown below.

type Animal={
name: string,
voice: ()=>string
}
interface MyPet {
petName: string;
}
class Cat implements Animal, MyPet{

constructor(public name: string, public petName: string){
}
voice(){
return this.name + ' meaw';
}
}
class Dog implements Animal, MyPet{ constructor(public name: string, public petName: string){
}
voice(){
return this.name + ' woof';
}
}
let c= new Cat("cutty", "sweety");console.log(c.voice());

Differences

Readonly Access modifier

The “readonly” keyword is enforced by TypeScript. You can mark class property, function parameter and type property as “readonly”. Readonly properties once we assign value we can not change the data.

See the example below of type “CirclePoint” where we have added readonly “x”. If we try to change “x” value then the compiler will show the error.

type CirclePoint = {
readonly x: number,
y: number
}
let p: CirclePoint = {x:1,y:2};p.x=0; //Result - error as readonly
p.y=10;

Let’s add readonly properties in the class. We can also combine readonly with public, private ad protected access modifiers as shown below.

class Animal{
readonly name: string;
private readonly type: string;
constructor(_name: string){
this.name=_name;
}
}
let dog = new Animal("DOG");
dog.name ="Tommy"; //Result - error as readonly

Add readonly to function parameter(input). Let’s create a function reverseSortedArray(input: number[]). This function accepts “input” of the type array of numbers. This function returns the reverse sorted array.

The output should be as below.

let start = [1,2,3,4,5];

console.log(reverseSortedArray(start)); //Result - [5,4,3,2,1]

We have implemented this functionality using the reverse() method of the array as shown below.

function reverseSortedArray(input: number[]) : number[]{
return input.reverse();
}

But remember reverse() function mutates the array. This means it changes the original array as well.

let start = [1,2,3,4,5];

console.log(reverseSortedArray(start)); //Result - [5,4,3,2,1]
console.log(start);//[5,4,3,2,1] It changed the original "start" array as well.

But we should not mutate/change our input parameters. In this case, the TypeScript compiler does not show any error/warning.

To make the input parameter of the function immutable we can use the “readonly” keyword as below.

function reverseSortedArray(input: readonly number[]) : number[]{
return input.reverse(); //Error - Property 'reverse' does not exist on type 'readonly number[]'
}

To solve this we should use the slice() function of the array which returns the copy of the array and does not change the original array as shown below.

function reverseSortedArray(input: readonly number[]) : number[]{
return input.slice().reverse();
}
let start = [1,2,3,4,5];console.log(reverseSortedArray(start)); //Result - [5,4,3,2,1]console.log(start);//Result- [1,2,3,4,5]

Thus by adding “readonly” we can make our input parameter of a function immutable.

TypeScript includes the readonly keyword that makes a property read-only in the class, type or interface.

Readonly Array and Tuples

An example of a readonly tuple is as below.

type PointTuples = readonly [number, number];function move( point: PointTuples, x: number, y: number){        return [point[0] + x, point[1] +y];}let tppt: PointTuples = [10,10];let moved = move(tppt,2,3);console.log(tppt); // [12,13]console.log(moved); // [12,10]

Structural Typing

To understand structural typing we should undersand following 4 concepts.

  1. What is Type checking?
  2. Static VS dynamic
  3. Nominal Vs Structural
  4. Duck Typing

Type checking

Type checking means checking type equivalent.

Type equivalent is checked for input parameters, assignment and return value.

Input Parameters

function baz(x){};baz(myValue);

Type checking → Is “myValue” is type equivalent to what baz() wants to receive?

This question is called at a function call such as baz(myValue).
Does the type of “myValue” and “baz” function input parameter is equivalent?

Assignment

x = y;

Type checking → Is the value “y “holds type-equivalent to what “x” wants to hold? If “x” is number and “y” is string then there is a problem.

Return value

const myStrings=["a"];function bar(): string[]{  
return myString;
}

Type checking → Is “myStrings” type-equivalent to what “bar()” states it will return?

Static Vs Dynamic

Type checking can be performed at compile time or runtime.

Static → Type checking is at Compile time
e.g. java, C# , Typescript, Scala

Dynamic → Type checking is at runtime.
e.g. Javacript, Python, Ruby, PHP, perl

Nominal Vs Structural

Nominal is all about Names. C# and Java has nominative types. But TypeScript is Structural Type language.

Example, in C# we have defined 2 classes having same structure. As shown below “User” and “Product” are 2 classes having id of type string. Both these classes have same structure. But its objects are not assignable to each other.

//C#class User{
public String id;
}

class Product{
public String id;
}

Bar bar = new Foo(); //Error

In TypeScript, it is possible. We can assign objects to each other if stucture of both is same. Typescript is structural it does NOT care about the name of type. Structural Type systems are all about structure or Shape and NOT Names of type.

User and Product both has same structure and hence can assign each other. Both has same type compatibility.

type User={id: string};
type Product={id: string};
let user: User={id:'user-1234'};let product: Product={id: 'product6666'};//Both User and Product has same structure and same type compatibility. So we can assign it to eah otheruser = product;product = user;

Let’s take one more example. We have defined 2 types “Point2D” and “Point3D”.
Point2D has x and y properties of type number.
Point3D has x,y and z properties of type number.

type Point2D = {x:number, y:number;}type Point3D = {x:number, y:number, z:number};

let’s define variables

let p2D: Point2D={x:10,y:20};let p3D: Point3D={x:10,y:20,z:30};

If we assign p3D to p2D then does TypeScript allow? Yes, it will allow.

As p2D (Point2D) need x and y and p3D (Point3D) has x, y and z. While assigning typeScript check the structure for type checking. p3D has all properties that p2D needs so assignment is allowed. We can also say extra information is Ok so we can assign Point2D to Point3D.

p2D  = p3D; //Allowed

Can we assign p2D to p3D? No.

As p3D (Point3D) need x,y and z. p2D (Point2D) has only x,y. “z” in p3D is mandatory property so TypeScript will allow this assignment. Left hand side type is minimum structure requirement that needs from right hand side.

p3D  = p2D; //Not Allowed

Let’s write a function takesPointsAs2D(p: Point2D) whih takes an argument of type Point2D.

function takesPointsAs2D(p: Point2D){
}

If we pass Point3D type variable to function takesPointsAs2D() then it is allowed due to structure typing. It is called duck typing. e.g. point3D walk and quack like point2D.

takesPointsAs2D(p3D); //Allowed

Now Let’s write function takesPointsAs3D(p: Point3D) which takes an argument of type Point3D.

function takesPointsAs3D(p: Point3D){
}
takesPointsAs3D(p3D); //AllowedtakesPointsAs3D(p2D); //Not Allowed z is missing in p2D

TypeScript is Structurally typed language. In structurally-typed languages, values are considered to be of equivalent types if all of their component features are of the same type. TypseScript is all about structures.

That’s it for part-1.

--

--