TypeScript Notes


TypeScript Roadmap : Click )

TypeScript is a strongly typed, object oriented, compiled language. It was designed by Anders Hejlsberg (designer of C#) at Microsoft. TypeScript is both a language and a set of tools. TypeScript is a typed superset of JavaScript compiled to JavaScript. In other words, TypeScript is JavaScript plus some additional features like Types and Object-Oriented Programming. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It offers classes, modules, and interfaces to help you build robust componentsTypeScript uses compile-time type checking. Which means it checks if the specified types match before running the code, not while running the code.


 // Install typescript compiler
 npm install -g typescript

// Get globally installed packages path. Add output to your PATH variables.
npm config get prefix

NOTE : In regular Javascript since we dont have types we could acess any property or function of objects with the dot operator (".") even if that property or function is not available on that variable. This way alot of errors would bypass the compilation process and only to be caught at runtime. With Typescript , since we add types to each variables, such runtime errors are avoided as we can only acess properties or functions available to a specific data type.


  // JavaScript code
 
  const user = {
    name: 'John',
    age: 30
  };
 
  console.log(user.address); // undefined
 
  // No error, but cause runtime error if age is used in an arithmetic operation                                  
  user.age = 'thirty';
 
//------------------------------------------------------------------------------------

// TypeScript code

interface User {
    name: string;
    age: number;
  }
 
  const user: User = {
    name: 'John',
    age: 30
  };
 
  // Compilation error: Property 'address' does not exist on type 'User'.
  console.log(user.address);
 
  // Compilation error: Type 'string' is not assignable to type 'number'.
  user.age = 'thirty';
 

Some of the benefits of using TypeScript are as followed :

  • Makes code more readable to developers : TypeScript's type system helps to document the code and make it easier to understand, especially for developers who are new to the project.
  • Adds a type system to JavaScript : TypeScript extends JavaScript by adding a type system, which helps to catch type-related errors at compile time instead of at runtime. This helps to prevent unexpected bugs and makes the code more robust.

  • Catches errors early at compile time rather than at runtime : By catching type-related errors early in the development process, TypeScript helps to save time and effort that would otherwise be spent debugging and fixing errors at runtime.

TypeScript code cannot be executed on any browser directly. The program written in TypeScript always ends with JavaScript. Hence, we only need to know JavaScript to use it in TypeScript. Transpilers or source-to-source compilers, are tools that read the sourcecode written in one programming language and produce the equivalent code in another programming language. 

Typescript also has a Transpiler of its own to convert Typescript to Javascript.The code written in TypeScript is compiled and converted into its JavaScript equivalent for the execution. This process is known as Transpilation.

NOTE : The file extension for typescript files is ".ts" extension, which after transpilation generates the regular ".js" file.

---------------------------------------------------------------------------------------------------------------

(Useful: 1] Click)


Typescript Compiler (TSC)

Tsc stands for `TypeScript compiler` and is a simple CLI tool included in Typescript itself, allowing you to compile ts files into js. Below are some common tsc commands.


# Shows all tsc options
tsc --all
# Generates JS from 'rootDir' into 'outDir' as mentioned in tsconfig.json file
tsc

# Generates the tsconfig.json file
tsc --init

# Generates JS for provided .ts file with compiler defaults
tsc index.ts

# Watches for changes in .ts files and updates the JS files
tsc --watch

# Generates JS for any .ts files in the folder src, with the default settings
tsc src/*.ts


tsconfig.json

The tsconfig.json file corresponds to the configuration of the TypeScript compiler (tsc). It is a configuration file in TypeScript that specifies the compiler options for building your project. It helps the TypeScript compiler understand the structure of your project and how it should be compiled to JavaScript. 

It also signifies that the directory in which it is kept is the root of TypeScript project. The tsconfig.json file specifies the root files and the compiler options required to compile the project. The TSC is expected to be executed based on the options mentioned inside the config file.


# Generates the tsconfig.json file
tsc --init

Below are some important options to be mentioned inside the tsconfig.json file :

  • target - The javascript version to which .ts files are transpiled, default is ES6.
  • rootDir - Path to the folder where .ts files are stored.
  • outDir - Path to the folder where JS files should be stored after transpilation.
  • allowJS - Tells the TypeScript compiler to process JavaScript files as well as TypeScript files, allowing us to use regular Javascript with Typescript. 
  • module - Sets which module system to use (CommonJs, UMd, AMF, Ecmascript). Use commonJs for NodeJs projects.
  • moduleResolution - Sets the module resolution strategy to use during transpilation.
  • removeComments - Strips all comments from TypeScript files when converting into JavaScript. 


TS-Node

Traditionally, TypeScript code needs to be compiled to JavaScript before it can be executed in a Node.js environment. However, with "ts-node", TypeScript code can be executed directly in a Node.js environment, as "ts-node" compiles TypeScript on-the-fly, meaning that it dynamically converts TypeScript code to JavaScript at runtime, without the need for a separate compilation step.


 // Install ts-node package
 > npm install -g ts-node

 // Execute Typescrit directly
 > ts-node index.ts

 // Activate REPL CLI
 > ts-node

Another added bonus to ts-node is being able to use a TypeScript REPL (read-evaluate-print loop) similar to running node without any options. This TypeScript REPL allows you to write TypeScript in command-line and is super handy for quickly testing something out.

---------------------------------------------------------------------------------------------------------------

Type Annotation/Assertion

TypeScript is a typed language, where we can specify the type of the variables, function parameters and object properties. We can specify the type by using the ":type" after the name of the variable, parameter or property. There can be a space after the colon.


var age: number = 32; // number variable
var name: string = "John"; // string variable
var isUpdated: boolean = true; // Boolean variable

Type annotations are used to enforce type checking. It is not mandatory in TypeScript to use type annotations. However, type annotations help the compiler in checking types and helps avoid errors dealing with data types.

---------------------------------------------------------------------------------------------------------------

Primitive Data Types

Like JavaScript and any other language, TypeScript also provides basic data types to handle numbers, strings, etc. Some common data types in TypeScript are as followed :

  • StringIt is used to represent a sequence of characters.
  • NumberIt represents both Integer as well as Floating-Point numbers.
  • BooleanRepresents true and false
  • AnyIf variable is declared with "any" data-type then any type of value can be assigned to that variable. Use it to skip type checks like regular JS.
  • Unknown - Similar to "any" but alot less permissive than "any" data type.
  • Never - It type represents the data type of values that never occur.
  • Null & UndefinedIt is used when an object does not have any value.
  • VoidGenerally used on function return-types if a function does not return any value.


// Main.ts

var firstname:string = "Deepesh";  // string
var age:number = 19;              // number
var GCPA:number = 9.88;          // number
var isMarried:boolean = false;  // boolean

//------------------Null-------------------------

// variable of type null can assign only null value.
let nullValue: null = null;

// You can assign null to any data type
let numericValue: number = null;

//------------------Any-------------------------

// Gives Error !
var firstname:string = "Deepesh"
firstname = 1001;
console.log(firstname)

// This gives No Error !
var firstname:any = "Deepesh";
firstname = 1001;

//--------------Void-----------------------------

function doSomething():void{
    console.log('Hello!');
}

NOTE : undefined is a variable that refers to something that doesn't exist, and the variable isn't defined to be anything. null is a variable that is defined but is missing a value.


Any & Unknown Type

The any type is essentially an escape hatch from the type system. As developers, this gives us a ton of freedom. TypeScript lets us perform any operation we want on values of type any without having to perform any kind of checking beforehand. Because of that, TypeScript considers all of the following operations on an "any" type variable to be type-correct by default. In many cases, this is too permissive. Using the any type, it's easy to write code that is type-correct, but problematic at runtime. We don't get a lot of protection from TypeScript if we're opting to use any.


let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

The 'unknown' type is similar to the 'any' type in a way that we can assign any value to it. There are 2 major differences 'unknown' and 'any' which are as followed :

  • The variable of 'unknown' type is only assignable to variables of 'any' type or 'unknown' type itself, this makes sense after all, we don't know anything about what kind of value is stored.
  • We cannot perform arbitrary operations on 'unknown' type variable since none of these operations are type-correct anymore and we can’t access any properties on unknown values, nor can we call or construct them.


let value: unknown;

value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(); // OK
value = Symbol("type"); // OK

//---------------------------------

let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error

//-----------------------------------

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

You can't operate directly on variables of type unknown. We have to give TypeScript information to narrow the type so that it can be used. Typescript doesn't allow you to use a variable of unknown type unless you either cast the variable to a known type or narrow its type. Type narrowing is the process of moving a less precise type to a more precise type.


// Gives Error !
function add1(a: unknown, b: unknown) {
  return a + b;
}

// This works !
function add2(a: unknown, b: unknown) {
    if (typeof a === "number" && typeof b === "number") {
        return a + b;
    }
    return 0;
}

// This works too !
function add3(a: unknown, b: unknown) {
    let a1 = a as number
    let b1 = b as number
    return a1+b1;
}

In summary, a variabe of type 'unknown' is similar to any but not operable until we narrow down its data type.


Never Type  (Useful : 1] Click)

The "never" type represents the data type of values that never occur. The never type is used when you are sure that something is never going to occur. The never type is a type that contains no values. Because of this, you cannot assign any value to a variable with a never type. 

Typically, you use the never type to represent the return type of a function that always throws an error or a function that never returns at all and has no reachable point and there mainly 2 cases where a function has no reachable point :

  • A Function only throws an Error.
  • A Function is running an indefinite loop inside it.
NOTE : No value of any other type can be assigned to a variable of never type.


 // Gives Error !
 var someVal: never = 111;
 var someVal2: never = "Hi";
 var someVal3: never = null;


 function throwError(message: string): never {
     throw new Error(message);
 }

 function neverStop(): never {
    while(true){
        console.log("Hi")
    }
}

In a function expression or arrow function with no return type annotation, if the function has no return statements, or only return statements with expressions of type never, and if the end point of the function is not reachable, the inferred return type for the function is never.

Difference between void and never data types is as follows :
  • A function that doesn't explicitly return a value implicitly returns the value undefined in JavaScript. Although we typically say that such a function "doesn't return anything", it returns. We usually ignore the return value in these cases. Such a function is inferred to have a void return type in TypeScript.
  • A function that has a never return type never returns. It doesn't return undefined, either. The function doesn't have a normal completion, which means it throws an error or never finishes running at all.

---------------------------------------------------------------------------------------------------------------

Type Inference

TypeScript is a typed language. However, it is not mandatory to specify type of every variable. TypeScript compiler can deduce the types of variables for us when there is no explicit information available in the form of type annotations, this process is called Type Inference, where we rely on Typescript to get the types right. Types are inferred by TypeScript compiler when :

  • Variables are initialized
  • Default values are set for parameters
  • Function return types are determined


// value inferred as string
var username = "deepeshdm"

// value inferred as number
var age = 19

// num1, num2 inferred as number
// and return type as number
function add(num1=0,num2=0){
    return num1+num2;
}

// num1, num2 inferred as (string,number)
// and return type as void
function sub(num1="0",num2=0){
    console.log(num1,num2)
}

For more complex objects like Arrays, TypeScript compiler looks for the most common type to infer the type of the object. If it does not find any super type that can encompass all the types present in the array. In such cases, the compiler treats the type as a union of all types present in the array. 


// array of type number
var arr1 = [1,2,3,4]

// array of type (number|string)
var arr2 = [1,2,3,4,"hi"]

// array of type (number|string|boolean)
var arr3 = [1,2,3,4,"hi",false]


Contextual Typing

Typescript also has the ability to infer the type of variables based on their context or location. The best example is when we type event objects.


// "e" is inferred as type MouseEvent due it's context
let button1 = document.getElementById("btn");
button1?.addEventListener("click",(e)=>{
    console.log(e.type)
})


//-----------------------------------------------------

// "e" is inferred as type "any"
function myHandler(e){
    console.log(e.type)
}

let button2 = document.getElementById("btn");
button2?.addEventListener("click",myHandler)


---------------------------------------------------------------------------------------------------------------

Functions

Typescript has a specific syntax to define functions with arguments and return types. The syntax for creating functions in TypeScript is the same, except for one major addition: You can let the compiler know what types each argument or parameter should have. 


// Syntax
// function someFunc(arg:<type>,...) : <return-type> {  }

function doSomething(fname:string,age:number):void{
    console.log(`Hello ${fname},you are ${age} years old !`)
}

doSomething("Deepesh",19);  // Ok
doSomething("Deepesh","19");  // Error

NOTE : If no return type is defined, TypeScript will attempt to infer it through the types of the variables or expressions returned.


function add(a: number, b: number): number {
    return a + b;
}

const multiply = (a: number, b: number): number => {
    return a * b;
};

NOTE : If no parameter type is defined, TypeScript will default to using "any"data type.


Function Overloading

Function Overloading in TypeScript allows multiple functions with the same name but with different parameters to be defined. The correct function to call is determined based on type, order & number of arguments passed to the function at runtime.


function add(a: number, b: number): number;

function add(a: string, b: string): string;

function add(a: any, b: any): any {
  return a + b;
}

console.log(add(1, 2));    // 3
console.log(add("Hello", " World"));    // "Hello World"

---------------------------------------------------------------------------------------------------------------

Non-Primitive Data Types

Below are some common non-primitive data types supported in Typescript :

  • Array
  • Tuples
  • Enum
  • Objects

Arrays

An array is a homogenous collection of similar type of elements which have a contiguous memory location. In Typescript we can define what type of values can be pushed inside the arrays by definig their data type.


// Syntax
// var name :<dtype>[] = [...]

var scores: number[] = [33,44,54,12,10]
scores.push(100)  // ok
scores.push(46)  // ok
scores.push("Hi")  // Error

NOTE : The readonly keyword can prevent arrays from being changed, so we'll get an error if we try to push something or change any value inside of it.


// Syntax
// var name : readonly <dtype>[] = [...]

var scores: readonly number[] = [33,44,54,12,10]
scores.push(100)  // Error
scores.push(46)  // Error
scores.push("Hi")  // Error
scores[2] = 33 // Error


Tuples

tuple is an array with a pre-defined length which allows us to store a collection of values of varied types which could'nt be done with arrays.


 // Syntax
 // var tuple_name = [value1,value2,value3,…value n]

 var things = ["Deepesh",101,[1,2,3],true, {name:"Rohan",lastname:"Singh"}]
 console.log(things)

We can also explicitly define the types of each index element. If you do so you have to maintain the order of elements.


// define tuples
let ourTuple: [number, boolean, string];
// initialize correctly
ourTuple = [5, false, 'Coding God was here'];
// initialize incorrectly throws an error
ourTuple = [false, 'Coding God was mistaken', 5];


let things1: [number,string,boolean] = [22,"Deepesh",false]
 // Error : Boolean required not Number
let things2: [number,string,boolean] = [22,"Deepesh",22]


The readonly keyword can prevent tuple from being changed, so we'll get an error if we try to push something or change any value inside of it.

 
 // define tuples
 let ourTuple: readonly [number, boolean, string];

 // initialize correctly
 ourTuple = [5, false, 'Coding God was here'];

ourTuple.push("Hi") // Error
ourTuple[3] = 111;  // Error


Enums

In TypeScriptenums, or enumerated types, are data structures of constant length that hold a set of constant values. Each of these constant values is known as a member of the enum. Enums are useful when setting properties or values that can only be a certain number of possible valuesThere are 3 types of enums in Typescript, which are as followed :

  • Numeric Enums - These store values as numbers. By default, enums will initialize the first value to 0 and add 1 to each additional value.
  • String Enums - These store string values and need to be initialized with values.
  • Heterogeneous Enums - These contain both strings and numbers as values.

// NUMERIC ENUM - default
enum Directions1 {
    Top,
    Bottom,
    Left,
    Right
}

// NUMERIC ENUM - initialized
enum Directions2 {
    Top = 4,
    Bottom = 5,
    Left = 10,
    Right = 20
}

console.log(Directions1.Top);      // 0
console.log(Directions1.Bottom);  // 1
console.log(Directions1.Right);  // 3

console.log(Directions2.Top);      // 4
console.log(Directions2.Right);  // 20

//------------------------------------------------------------------------------------

// STRING ENUM
enum Directions3 {
    Top = "top",
    Bottom = "bottom",
    Left = "left",
    Right = "right"
}
console.log(Directions3.Top);     // "top"
console.log(Directions3.Right);  // "right"

//------------------------------------------------------------------------------------

// HETEROGENOUS ENUM
enum Directions {
    Top = "top",
    Bottom = 1,
    Left = 3,
    Right = "right"
}

console.log(Directions.Top);   // "top"
console.log(Directions.Left); // 3

Example] Below the getCheese() can only accept values present inside enum.

enum Cheese {
    cheddar = "Cheddar",
    gouda = "Gouda",
    goat = "Goat",
    blueMould = "BlueMould"
}


function getCheese(cheeeseType: Cheese, servings: number):void {
    console.log(`You want ${servings} servings of ${cheeeseType} cheese !`)
}


getCheese(Cheese.cheddar, 100)
getCheese(Cheese.blueMould, 200)
getCheese("American Cheese", 20)  // Error
getCheese("blueMould", 50)       // Error


Object Type

An object is an instance which contains set of key value pairs. The values can be scalar values or functions or even array of other objects. When adding a property to the object, you also need to define the type of each property.

 
 // syntax
 // var obj : {property:<type>,...} = {property:value,...}

 var obj : {fname:string,age:number,isMarried:boolean}={
    fname:"Deepesh",
    age:19,
    isMarried:false,
 }
 
obj.fname = "Rohan"; // ok
obj.age = "33"; // Error

We can also create "Optional Properties" by defining them inside the types with an "?" but not inside the object values, and later add them dynamicaly.


 var obj: {fname:string,age:number,isMarried:boolean}={
    fname:"Deepesh",
    age:19,
    isMarried:false,
 }
 
 obj.occupation = "Programmer"; // Error

//------------------------------------------------------------------------------------

var obj: {fname:string,age:number,isMarried:boolean,occupation?:string} ={
    fname:"Deepesh",
    age:19,
    isMarried:false,
 }
 
 obj.occupation = "Programmer"; // Ok


The "in" keyword

In TypeScript, the in keyword is used to check if a property exists in an object. It is often used in conjunction with the if statement as type guard.


const Person = {
    firstname: "Deepesh",
    lastname: "Mhatre",
    isMarried: false
}

if ("firstname" in Person) {
    console.log("Firstname present in object !")
} else {
    console.log("Firstname NOT in object !")
}

// Output : Firstname present in object !


---------------------------------------------------------------------------------------------------------------

TypeOf Operator / Type Guard

A typeOf keyword returns the type of an identifier in TypeScript. It also acts as a Type Guard narrowing the type of the variable in the scope where we use it. It is used to check the type of a variable. It returns a string representing the type of the variable.


let fname = "deepeshdm";
let age = 19
let married = false;

console.log(typeof fname)
console.log(typeof age)
console.log(typeof married)

// string
// number
//boolean

Let's say we want to perform different actions based upon whether input is a number or a string. In this case, we will use Javascripts type guards to check if it's a string or number, as shown below.


function someFunc(input: string | number) {
  if(typeof input === "string") {
    // do something with the string
    console.log("input is a string");
  }
 
  if(typeof input === "number") {
    // do something with number
    console.log("input is a number");
  }
}


---------------------------------------------------------------------------------------------------------------

Custom Types

In TypeScript, an interface and a type alias are 2 different ways of defining a custom type with given name. While both interfaces and type aliases can be used to define custom types, interfaces are typically used to define object shapes, while type aliases are more commonly used to define union types, complex types, and other types that don't fit the object shape pattern.


1] Type Aliases

TypeScript allows types to be defined separately from variables that use them. Type Aliases allow defining types with a custom name (an Alias) which can be also reused again. Type Aliases can be used for primitives like string or more complex types such as objects and arrays. We use the "type" keyword to create an type alias.


// Define Types

type CarYear = number
type CarType = string | number
type isAvailable = boolean

type Car1 = { year: CarYear, cartype: CarType, isAvail: isAvailable }

type Car2 = { year: string, cartype: string|number, isAvail: boolean }

//------------------------------------------------------------------------------------

// Use defined Types

const carYear: CarYear = 2001
const carType: CarType = "Toyota"
const isAvail: isAvailable = true

const Mustang: Car1 = {
    year: 1980,
    cartype: "Sports",
    isAvail: false
};

const RangeRover: Car2 = {
    year: "1995",
    cartype: "SUV",
    isAvail: true
};

We can also combine multiple type aliases to create a new type alias using the "&" intersection operator as shown below.


// define types

type Pet = {
    name: string;
    age: number;
};


type Dog = Pet & {
    breed: string;
};


type Bird = Pet & {
    canFly: boolean
}

//------------------------------------------------------------------------------------

// use the types

const Animal1: Dog = {
    name: "Lucy",
    age: 4.5,
    breed: "Bulldog",
}

const Animal2: Bird = {
    name: "Baaz",
    age: 2,
    canFly: true
}


2] Interfaces

An interface is an OOP concept and is similar to Type alias except it only applies to objects. TypeScript allows you to specifically add type to objects using an interface that can be reused by multiple objects. Interfaces in TypeScript are created by using the "interface" keyword

 
interface Car {
    carYear:number,
    type:string,
    model:string,
    isFast:boolean
    goFast():void
 }

 var BMW:Car ={
    carYear:2012,
    type:"Sports",
    model:"Pagani",
    isFast:true,
    goFast:()=>{ console.log("Going Fast !")}
 }

We can combine multiple interfaces using "extends" keyword, as shown below :


interface CarInfo {
    carYear:number,
    type:string,
}

interface Car extends CarInfo{
    model:string,
    isFast:boolean
}

var BMW:Car ={
    carYear:2012,
    type:"Sports",
    model:"Pagani",
    isFast:true
}


---------------------------------------------------------------------------------------------------------------

Union Type

The Union Type allows us to use more than one data type for a variable or a function parameter. Union Types in TypeScript allow you to specify multiple possible types for a single variable or parameter. A union type is written as a vertical bar "|" separated list of types.


// Syntax
// (type1 | type2 | type3 | .. | typeN)

var someVal : (string|number) = 100; // Ok
someVal = "Hi";                     // Ok
someVal = true;                    // Error

var someVal2 : (string|number|boolean) = 100; // Ok
someVal = "Hi";                              // Ok
someVal = true;                             // Ok

//-------------------------------------------------------------------------------------

function displayType(code: (string | number)){
    if(typeof(code) === "number")
        console.log('Code is number.')
    else if(typeof(code) === "string")
        console.log('Code is string.')
}

displayType(123); // Output: Code is number.
displayType("ABC"); // Output: Code is string.
//Compiler Error: Argument of type 'true' not assignable to parameter
displayType(true);

We can also create more flexible types with the help of Unions in Typescript, such that if we have two different types of objects that share some similar properties, we can define a union type of them so the resulting type can contain any combination of properties from either of types.


type SuperPowers1 = {
    power1: string,
    power2: string,
    power3: string,
}

type SuperPowers2 = {
    power4: string,
    power5: string,
    power6: string,
}

type allPowers = SuperPowers1 | SuperPowers2

let obj1: allPowers = {
    power1: "Flying",
    power2: "Running",
    power3: "Digging",
    power4: "TimeTravel",
    power5: "HighIQ",
    power6: "YoungForever",
}

let obj2: allPowers = {
    power1: "Flying",
    power2: "Running",
    power3: "Digging",
    power6: "YoungForever",
}

let obj3: allPowers = {
    power1: "Flying",
    power4: "TimeTravel",
    power5: "HighIQ",
    power6: "YoungForever",
}


Intersection Type

An intersection type creates a new type by combining multiple existing types. The new type has all features of the existing types. To combine types, you use the "&" operator.


interface BusinessPartner {
    name: string;
    credit: number;
}

interface Identity {
    id: number;
    name: string;
}

interface Contact {
    email: string;
    phone: string;
}

//-------------------------------------------------------------------------------------

// combine multiple types
type Employee = Identity & Contact;
type Customer = BusinessPartner & Contact;


let e: Employee = {
    id: 100,
    name: 'John Doe',
    email: 'john.doe@example.com',
    phone: '(408)-897-5684'
};

let c: Customer = {
    name: 'ABC Inc.',
    credit: 1000000,
    email: 'sales@abcinc.com',
    phone: '(408)-897-5735'
};



type BusinessPartner = {
    name: string;
    credit: number;
 }
 
 type Identity = {
    id: number;
    name: string;
 }

 type Contact = {
    email: string;
    phone: string;
 }

//-------------------------------------------------------------------------------------

 // combine multiple types
 type Employee = Identity & Contact;
 type Customer = BusinessPartner & Contact;


 let e: Employee = {
    id: 100,
    name: 'John Doe',
    email: 'john.doe@example.com',
    phone: '(408)-897-5684'
 };

 let c: Customer = {
    name: 'ABC Inc.',
    credit: 1000000,
    email: 'sales@abcinc.com',
    phone: '(408)-897-5735'
 };


---------------------------------------------------------------------------------------------------------------

Explicit and Implicit Assignment

Using TypeScript can provide many benefits, such as reducing bugs or self-documenting codebase. But it can also cause dilemmas, such as whether to allow TypeScript to infer types on its own or to explicitly annotate the type, as in other programming languages.

There are 2 ways to add type annotations in Typescript as followed :

  • Explicit Type Annotation - It's when we ourself provide the type using a special TypeScript syntax of ":" colon mark.
  • Implicit Type Annotation -It's when TypeScript infers the type on its own without us defining any type, based on a variable's initial value and look just like regular JavaScript.


// Implicit Type Annotation
// NOTE : Typescript infers the assigned value and sets it as the data type.

let fname = "deepeshdm";
fname = "deepeshdm"  // Ok
fname = 100;  // Type Error

//-------------------------------------------------------------------------------------

var obj = {
    fname : "deepeshdm",
    age : 19,
    married : false
}

obj.fname = "deepeshdm"  // Ok
obj.fname = 111;  // Type Error
obj.age = 34  // Ok

NOTE : The Implicit Type annotation may look like regular Javascript but it's Typescript as in any case the variables and objects are Type-safe.

---------------------------------------------------------------------------------------------------------------

Utility Types

In TypeScript, utility types are pre-defined type transformations that allow developers to easily manipulate and transform existing types. Some of the most commonly used utility types in TypeScript are the following :

  • Partial<T> : It creates a new type that has all the properties of the original type T, but all the properties are optional.
  • Required<T> : It creates a new type that has all the properties of the original type T, but all the properties are required.
  • Readonly<T> : It creates a new type that has all the properties of the original type T, but all the properties are read-only.
  • Pick<T,K> : It creates a new type that has only the specified properties K of the original type T.
  • Omit<T,K> : It creates a new type that has all the properties of the original type T, except for the specified properties K.


1] Partial<T>

By default win Typescript, an object must specify values for all the properties mentioned inside its Type. The Partial type in TypeScript allows you to make all properties of a type optional. This is useful when you need to create an object with only a subset of the properties of an existing type.


type Animal = {
    name:string,
    breed:string,
    age:number,
    canFly:boolean,
    canSwim:boolean
}

// ERROR : Missing Properties from Type Animal
const pet1 : Animal = { name:"Lucy", breed:"BullDog" }

// Works !
const pet2 : Partial<Animal> = { name:"Lucy", breed:"BullDog" }


2] Required<T>

The Required<T> is a utility type in TypeScript that makes all properties of a given type T required. It's useful when you have an object or a function that requires all properties to be present, and you want to ensure that all required properties are provided.


interface Person {
    name: string;
    age?: number;
}

function greet(person: Required<Person>) {
    console.log(`Hello, ${person.name}! You are ${person.age} years old.`)
}

// Error: Property 'age' is missing in type '{ name: string; }'
// but required in type 'Required<Person>'
const p1: Required<Person> = { name: 'John' };
greet(p1);

// Works !
const p2: Required<Person> = { name: 'John', age: 20 };
greet(p2)


3] ReadOnly<T>

Readonly constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned.


interface Person {
    fname: string,
    lname:string,
    age:number
}

const p1 : Readonly<Person> = {
    fname:"Deepesh",
    lname:"Mhatre",
    age:20
};

// Cannot assign 'age' because it is a read-only property.
p1.age = 30;


4] Pick<T,K>

The Pick<T,K> is a utility type in TypeScript that creates a new type by picking a subset of properties from an existing type T, based on a set of property names K. It is useful when we want to accept an object with only some of its properties.


type Person = {
    fname: string,
    lname: string,
    age: number,
    address: string,
    isMarried: boolean
}

const p1: Pick<Person, "fname" | "age" | "isMarried"> = {
    fname: "Deepesh",
    isMarried: false,
    age: 20,
    lname: "Mhatre", // Object can only contain specified 3 properties.
};

// Works !
const p2: Pick<Person, "fname" | "address"> = {
    fname: "Deepesh",
    address: "Mumbai"
};


5] Omit<T,K>

The Omit<T, K> is a utility type in TypeScript that creates a new type by omitting a subset of properties from an existing type T, based on a set of property names K. It is useful when we want to accept an object but exclude some of its properties.


interface Person {
    name: string;
    age: number;
    email: string;
    address: string;
}

type PersonWithoutEmail = Omit<Person, 'email'>;

const john: PersonWithoutEmail = {
    name: 'John',
    age: 30,
    email: 'john@example.com', // ERROR : Email is omitted
    address: '123 Main St',
};

// Works !
const johnWithoutEmail: PersonWithoutEmail = {
    name: john.name,
    age: john.age,
    address: john.address,
};


---------------------------------------------------------------------------------------------------------------

Classes

TypeScript is object oriented JavaScript. TypeScript supports object-oriented programming features like classes, interfaces, etc. A class in terms of OOP is a blueprint for creating objects. A class encapsulates data for the object. Typescript gives built in support for this concept called class. JavaScript ES5 or earlier didn’t support classes. Typescript gets this feature from ES6. 

In Typescript you use the "class" keyword to define a class, and the "new" keyword to create instances of that class. You can use the "constructor" keyword to define constructor function for the class which is executed everytime a new instance is created.

NOTE In TypeScript, when defining attributes or methods inside a class, you don't need to explicitly use the var, let, or const keywords to declare attributes, nor do you need to use the function keyword to define methods.


 class Student{

    // attributes
    fname:string;
    lname:string;
    age:number;

    // methods
    addNumbers(num1:number,num2:number):number{
        return num1+num2;
    }

    // constructor
    constructor(fname:string,lname:string,age:number){
        this.fname = fname;
        this.lname = lname;
        this.age = age;
    }
 }

 let student1 = new Student("Deepesh","Mhatre",19);
 console.log(student1.fname, student1.lname)
 var result = student1.addNumbers(10,20);
 console.log("Result : ",result);


Override Methods

The derived class can override the methods of parent class using the "override" keyword.


class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat(): void {
    console.log("Animal is Eating !");
  }
}


class Tiger extends Animal {
  constructor(name: string) {
    super(name);
  }
  override eat(): void {
    console.log("Tiger is Eating !");
  }
}

let animal1 = new Animal("Cat");
let tiger1 = new Tiger("Cat");
animal1.eat();
tiger1.eat();

// Animal is Eating !
// Tiger is Eating !


Inheritance

Just like in other programming languages, Typescript also provides us a way to inherit from other classes and Interfaces. We use the keyword "Implement" to inherit from Interfaces and "extends" to inherit from a class.

1] Implementing an Interface 

If you implement an Interface then the implementing class should strictly define all the properties and the functions of the Interface with the same name and data type inside its own class. If the implementing class does not follow the structure, then the compiler will show an error.


interface IEmployee {
  empCode: number;
  name: string;
  getSalary: (empCode: number) => number;
}

class Employee implements IEmployee {
  empCode: number;
  name: string;
  address:unknown;
  salary:unknown;

  constructor(code: number, name: string) {
    this.empCode = code;
    this.name = name;
    this.address = null;
    this.salary = null;
  }

  getSalary(empCode: number): number {
    return 20000;
  }
}

let emp = new Employee(1, "Steve");

NOTE : A class can inherit multiple interfaces but it can only inherit from one class.


2] Inheriting an class

We use the "extends" keyword to inherit from some other predefined class.


class Animal {
  name: string = "Animal";
  eat(): void {
    console.log(`${this.name} is Eating !`);
  }
}

class Tiger extends Animal {
  hunt(): void {
    console.log(`${this.name} is Hunting !`);
  }
}

let cat = new Tiger();
cat.eat();
cat.hunt();

// Animal is Eating !
// Animal is Hunting !

NOTE : The class whose members are inherited is called the "base class", and the class that inherits those members is called the "derived class".

If the base class consist a constructor function then the derived class must call the "super()" method inside it's constructor before using "this" keyword., it'll execute the constructor of parent class and initializes it's data memebers. 


class Employee {
  empName: string;
  empCode: number;

  constructor(name: string, code: number){
      this.empName = name;
      this.empCode = code;
  }
}

class SalesEmployee extends Employee{
  department: string;
 
  constructor(name: string, code: number, department: string) {
// call super first
      super(name, code);
      this.department = department;
  }
}

let emp = new SalesEmployee("John Smith", 123, "Sales");
console.log(emp.empName);  // Ok
console.log(emp.empCode); // Ok


Access/Visibility Modifiers

A class can control the visibility and access of its data members, this is done using access modifiers. There are 3 types of access modifiers in TypeScript : 

1] Public (default) - By default, all members of a class in TypeScript are public. All the public members can be accessed anywhere without any restrictions.


class Student {
  public name: string;
  age: number;
  married: boolean;

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

let student1 = new Student("Deepesh", 19, true);
console.log(student1.name); // Ok
console.log(student1.age); // Ok

2] Private - The private access modifier ensures that class members are visible only to that class and are not accessible outside the containing class.


class Student {
  private name: string;
  age: number;
  married: boolean;

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

let student1 = new Student("Deepesh", 19, true);
console.log(student1.name); // Error
// Property 'name' is private and only accessible within class 'Student'.
console.log(student1.age); // Ok

3] Protected - A protected class member can be only be accessed from its containing class and inside the class that inherit it.


class Employee {
  protected empName: string;
  empCode: number;

  constructor(name: string, code: number) {
    this.empName = name;
    this.empCode = code;
  }
}

class SalesEmployee extends Employee {
  department: string;

  constructor(name: string, code: number, department: string) {
    super(name, code);
    this.department = department;
  }

  getName(): string {
    return this.empName;
  }
}

let emp1 = new SalesEmployee("John Smith", 123, "Sales");
let emp2 = new Employee("Jason", 456);
// Accessing empName from outside the classes
console.log(emp1.empName); // Error
console.log(emp2.empName); // Error
// Accessing empName from inside the classes
console.log(emp1.getName()); // Ok

NOTE : If we add access modifiers to constructor arguments, they become class properties without us initializing them.


class Person {
  // name is a private member variable
  public constructor(private name: string, public age: number) {}

  getName(): string {
    return this.name;
  }
}

const person = new Person("Jane", 19);
console.log(person.getName());
console.log(person.age);


Static Members

When we use the "static" keyword on properties we define on a class, they belong to the class itselfThat means that we cannot access those properties from an instance of the class. We can only access the properties directly by referencing the class itself.


class Employee1 {
  empName: string;
  empCode: number;
  constructor(name: string, code: number) {
    this.empName = name;
    this.empCode = code;
  }
}

class Employee2 {
  static empName: string;
  empCode: number;
  constructor(name: string, code: number) {
    Employee2.empName = name;
    this.empCode = code;
  }
}

let emp1 = new Employee1("Deepesh",111);
let emp2 = new Employee2("Deepesh",111);
console.log(emp1.empName); // Ok
console.log(emp2.empName);  // Error
console.log(Employee2.empName);  // Ok


Abstract Class

An Abstract class is a class which cannot be initiated i.e we cannot create its instances but it can be inherited by other classes. It contain abstract and non-abstract methods in it, and the derived class must implement all abstract methods. An Interface is also an example of abstract class but with only abstract methods. To declare an abstract class, you use theabstractkeyword.


abstract class Human {
  name: string;
  constructor(name: string) {
    this.name = name;
  }

  abstract eat(): void;
  addNumbers(num1: number, num2: number): number {
    return num1 + num2;
  }
}


class Student extends Human {
  rollnumber: number;
  constructor(name: string, rollnumber: number) {
    super(name);
    this.rollnumber = rollnumber;
  }
  // implement abstract method
  eat(): void {
    console.log("Eating....!!");
  }
}


let student1 = new Student("Deepesh", 19);
student1.eat();
console.log(student1.addNumbers(10, 20));

NOTE : Abstract methods can only occur in an abstract class, that's why we also mark the class with "abstract" class.


InstanceOf Operator

The instanceOf operator  is a way to narrow down the type of a variable. It is used to check if an object is an instance of a class.


class Bird {
    fly() { console.log('flying...'); }
    layEggs() { console.log('laying eggs...'); }
}

const pet = new Bird();

// instanceof
if (pet instanceof Bird) {
    pet.fly();
} else {
    console.log('pet is not a bird');
}


---------------------------------------------------------------------------------------------------------------

Type Assertions & Type Casting

The term "type assertion" refers to the process of telling the TypeScript compiler to treat a value as if it has a specific type, even if the compiler cannot verify that the value has that type at compile-time. Type assertion is only relevant at compile-time, and does not affect the actual value or behavior of an object at runtime.

In a sense, you can think of type assertion as "fooling" the TypeScript compiler into treating a value as if it were of a specific type during compilation, even if TypeScript's type inference algorithm cannot determine the exact type of the value.

There are 2 ways to apply type assertions in TypeScript as follows :

  • The “angle-bracket” syntax : <T>value
  • The “as” syntax : value as T

NOTE : Type assertion is different than Type Casting or Conversion. Assertion only tells the compiler that an object is of specific type but does'nt actually convert it.

When you use type assertion to tell TypeScript the type of a value or expression, you are essentially overriding TypeScript's type inference algorithm for that specific value or expression. TypeScript will use the asserted type for type checking and type compatibility checks at compile-time, but the actual value of the object or value will not be modified or affected in any way.

// Type Conversion

let i = 12;
let j = i.toString()
console.log(typeof i)   // number
console.log(typeof j)  // string
console.log(j+5)      // 125

//------------------------------------------------------------------------------------

// Type Assertion

let x = 12;
let y = x as unknown as string

// Y is still a number type.
console.log(typeof x)   // number
console.log(typeof y)  // number
console.log(y+5)      // 17

NOTE : If you really want to perform type conversion and not just asert types, you can use built-in functions like the ".toString()", "Number()", "Boolean()" etc

A common use case for type assertion is when working with the DOM in TypeScript. The DOM API often returns elements or attributes as Element or HTMLElement, which are too general types in TypeScript. In such cases we can use type assertion to tell TypeScript the specific type of the element or attribute.

// Example 1: Using type assertion to access a specific element type
const myElement = document.getElementById("my-element") as HTMLInputElement;
console.log(myElement.value); // Output: ""

// Example 2: Using type assertion to access a specific event type
const myButton = document.getElementById("my-button") as HTMLButtonElement;
myButton.addEventListener("click", (event: MouseEvent)


NOTE : In TypeScript, type assertions can only be used with any or unknown types, as they are the only types that are flexible enough to allow any value to be assigned to them. If you want to assert a variable to a specific type, but the variable already has a specific type (or its type is inferred by TypeScript), you need to first convert it to any or unknown type before you can assert it again to your specific type.

By using unknown or any as an intermediary step, you can help ensure that the final type assertion is more accurate and less likely to result in runtime errors or unexpected behavior.

var userAge:number = 100;
var newAge:string = userAge as string  // Error
var newAge:string = <string>userAge;  // Error

//------------------------------------------------------------------------------------

var userAge:number = 100;
var newAge:string = userAge as unknown as string  // Ok
var newAge:string = <string><unknown>userAge;  // Ok

NOTE : The <> type of casting will not work with TSX, such as when working on React files as JSX components have similar syntax.


Non-Null Assertion Operator (!)

The non-null assertion operator (!) is a type assertion in TypeScript that allows you to tell the compiler that a value will never be null or undefined. In some cases, TypeScript may infer a type as potentially nullable at compile-time, even when you know that it won't be null or undefined.

By using the non-null assertion operator, you can tell TypeScript to treat the value as non-null, and the compiler will allow you to access its properties or call its methods without checking for null or undefined.


let myname: string | null = null;
let nameLength = myname.length; // Compile-Time Error : 'myname' is possibly 'null'.
console.log(nameLength)

//------------------------------------------------------------------------------------

let myname: string | null = null;
// Compiles Succesfully. Run-Time Error :  Cannot read properties of null
let nameLength = myname!.length;
console.log(nameLength)

It's worth noting that the non-null assertion operator should only be used when you are absolutely certain that the value is non-null or non-undefined. If the value is actually null or undefined at runtime, using the non-null assertion operator can lead to runtime errors or unexpected behavior.


Optional Chaining Operator

Optional chaining is a feature in JavaScript that allows you to safely access nested properties of an object without worrying about whether any of those properties are null or undefined. It uses the question mark (?.) operator to check if a property exists before attempting to access it's nested properties.

Without optional chaining, if we try to access a nested property of an object that is 'null' or 'undefined', we would get a runtime 'TypeError' and the program would stop executing. This can be especially problematic when dealing with deeply nested objects or objects that may have optional properties. Without optional chaining, we would need to manually use a lot of "if" statements to check for the existence of nested properties in an object.

By using optional chaining, we can safely access nested properties without worrying about whether they are 'null' or 'undefined'. If a property does not exist, the expression simply returns undefined and the program continues executing.


// main.js

const person = {
    name: "Deepesh",
    surname: "Mhatre",
    age: 19,
    skills: ["Programming", "Machine Learning"],
    introduce_yourself: function () { console.log("Hello,I am Deepesh !") }
}

console.log(person.name)  // Deepesh

// Check if the nested property exists before trying to access it
if (person.name && person.name.someNonExistingProperty &&
    person.name.someNonExistingProperty.getSomething) {
    console.log(person.name.someNonExistingProperty.getSomething);
} else {
    console.log(undefined);
}

// Check if each level of the nested properties exist before trying to access them
if (person.age && person.age.something1 && person.age.something1.something2 &&
    person.age.something1.something2.something3) {
    console.log(person.age.something1.something2.something3);
} else {
    console.log(undefined);
}

//----------------------------- WITH OPTIONAL CHAINING--------------------------------

const person = {
    name: "Deepesh",
    surname: "Mhatre",
    age: 19,
    skills: ["Programming", "Machine Learning"],
    introduce_yourself: function () { console.log("Hello,I am Deepesh !") }
}

console.log(person.name)  // Deepesh

// ERROR : Cannot read properties of undefined (reading 'getSomething')
console.log(person.name.someNonExistingProperty.getSomething)
console.log(person.name.someNonExistingProperty?.getSomething) // undefined

// Error: Cannot read properties of undefined (reading 'something2')
console.log(person.age.something1.something2.something3)
console.log(person.age.something1?.something2?.something3) // undefined




// main.ts

interface User {
    name: string;
    age?: number;
    address?: {
        street: string;
        city: string;
        zip?: number;
    };
}

const user1: User = {
    name: 'John',
    age: 30,
    address: {
        street: '123 Main St',
        city: 'New York',
        zip: 12345
    }
};

const user2: User = {
    name: 'Jane',
    age: 25
};


const user1Zip = user1?.address?.zip;
const user2Zip = user2?.address?.zip;
console.log(user1Zip)   // 12345
console.log(user2Zip)  // undefined // NO RUNTIME ERROR

NOTE : If a property or method accessed using optional chaining is null or undefined, the expression will short circuit and return undefined without evaluating the rest of the expression.


Nullish Coalescing Operator

The Nullish Coalescing operator (??) is a new operator introduced in ECMAScript 2020, used to provide a fallback value for variables that may be null or undefined. If the value is either 'null' or 'undefined', the operator returns the fallback value. If the value is not 'null' or 'undefined' the operator returns the actual value.


// main.js

let fetchedData;

if (fetchedData !== null && fetchedData !== undefined) {
    fetchedData = "Deepesh";
} else {
    fetchedData = "Value is Null !";
}

console.log(fetchedData); // "Deepesh"
fetchedData = null;

let result;
if (fetchedData !== null && fetchedData !== undefined) {
    result = fetchedData;
} else {
    result = "Fetched Value is Null !";
}

console.log(result); // "Fetched Value is Null !"

//------------------------------WITH NULLISH OPERATOR---------------------------------

let fetchedData = "Deepesh" ?? "Value is Null !";
console.log(name); // "Deepesh"

fetchedData = null;
const result = fetchedData ?? "Fetched Value is Null !";
console.log(result); // "Fetched Value is Null !"




// main.ts

interface User {
    name: string | null;
    age: number;
    email: string;
}

const user1: User = {
    name: 'John',
    age: 30,
    email: 'john@example.com'
};

const user2: User = {
    name: null,
    age: 25,
    email: 'jane@example.com'
};

// Using nullish coalescing to provide a fallback value
const user1Name = user1.name ?? 'Default User'; // 'John'
const user2Name = user2.name ?? 'Default User'; // 'Default User'

console.log(user1Name);
console.log(user2Name);


NOTE The Nullish Coalescing operator ?? is particularly useful when working with data fetched from an API because the API may not always return the data we expect. In cases where the data is missing or nullish, we can use the Nullish Coalescing operator to provide a default value.

---------------------------------------------------------------------------------------------------------------

Generics

Generics in OOP allow us to define a specification of a class or function that can work with multiple data type. When we design a generic, the data types of the function parameters or class isn't known - not until it is called or instantiated. The generics allow us to write code which is general and can be used for any data type, preventing us from rewriting the same code for multiple data types.

Generics allow you to write functions, classes, and interfaces that take one or more type parameters, which act as placeholders for the actual data types that will be used when the function, class, or interface is used.

Example] Below we write a generic function which can work with string and number type parameters at the same time.


// Arrow Function
const Add = <X, Y, Z>(input1: X, input2: Y, input3: Z[]): void => {
    console.log(`input1 is ${typeof (input1)} & input2 is ${typeof (input2)}`)
}

// Regular Function
function Add<X, Y, Z>(input1: X, input2: Y, input3: Z[]): void {
    console.log(`input1 is ${typeof (input1)} & input2 is ${typeof (input2)}`)
}

Add<string, number, number>("Deepesh", 100, [33, 55, 77]);
Add<string, string, string>("Deepesh", "100", ['apple', 'orange', 'kiwi']);
Add<number, number, any>(100, 100, []);

/*
input1 is string & input2 is number
input1 is string & input2 is string
input1 is number & input2 is number
*/

The generic type parameter is specified in angle brackets (<>) after the name of the class or function. A generic class can have generic fields (member variables) or methods.


class KeyValuePair<T,U>{
    private key: T;
    private val: U;

    constructor(key: T, val: U){
        this.key = key;
        this.val = val;
    }

    display():void {
        console.log(`Key = ${this.key}, val = ${this.val}`);
    }
}

let kvp1 = new KeyValuePair<number, string>(1, "Steve");
kvp1.display(); //Output: Key = 1, Val = Steve

let kvp2 = new KeyValuePair<string, string>("CEO", "Bill");
kvp2.display(); //Output: Key = CEO, Val = Bill

let kvp3 = new KeyValuePair<number,number>(22,33);
kvp3.display(); //Output: Key = 22, val = 33

Example] Below we write a generic object which can work with strings, numbers and array type properties at the same time.


// Generic Object

type Pair<T, U> = {
    first : T;
    second : U;
  };
 
 const obj1 : Pair<string, number> = { first: "foo", second: 42 }
 const obj2 : Pair<number, number> = { first: 100, second: 42 }
 const obj3 : Pair<boolean[], number[]> = { first: [true,false], second: [1,2,3] }
 
 console.log(obj1);
 console.log(obj2);
 console.log(obj3);

/*
 { first: 'foo', second: 42 }
 { first: 100, second: 42 }
 { first: [ true, false ], second: [ 1, 2, 3 ] }
*/

NOTE : In generics, type narrowing is not compulsory but Typescript avoids us from directly performing arbitrary operations on inputs of 'any' data type and enforces us to narrow down the types through Type guards and conditionals.


// GIVES ERROR !
function Add<X, Y>(num1: X, num2: Y) {
    return num1 + num2;
}

// WORKS !
function Add<X, Y>(num1: X, num2: Y) {
    if (typeof num1 == 'number' && typeof num2 == 'number') {
        return num1 + num2;
    }
    console.log("Inputs are not numbers !")
}

Example] We can also create complex interfaces with generics as shown below.


// main.ts

// Generic Interface
interface MobilePhone<T> {
    brand: string;
    model: string;
    specifications: T;
}

interface IOSpecs {
    display: string;
    camera: string;
    storage: string;
}

interface AndroidSpecs {
    display: string;
    camera: string;
    storage: string;
    battery: string;
}

const iphone: MobilePhone<IOSpecs> = {
    brand: "Apple",
    model: "iPhone 12",
    specifications: {
        display: "Super Retina XDR",
        camera: "Dual 12MP",
        storage: "64GB"
    }
};

const galaxy: MobilePhone<AndroidSpecs> = {
    brand: "Samsung",
    model: "Galaxy S21",
    specifications: {
        display: "Dynamic AMOLED 2X",
        camera: "Triple 12MP",
        storage: "128GB",
        battery: "4000mAh"
    }
};


---------------------------------------------------------------------------------------------------------------

Modules in Typescript

A module is a way to create a group of related variables, functions, classes, and interfaces, etc, which can be imported into other modules or Typescript files. In TypeScript, files containing a top-level export or import are considered modules. A module can be created using the keyword export and a module can be used in another module using the keyword import.

NOTE : In TypeScript, files containing a top-level export or import are considered modules, other files are considered regular scripts.

Example] Below we export functions and other objects in a module and import them into another file to be reused again.


// utils.ts

export const myVariable = "Hello, world!";

export function myFunction(name: string): string {
    return `Hello, ${name}!`;
}

export const myObject = {
    name: "John",
    age: 30,
    city: "New York"
};

export type MyType = {
    name: string;
    age: number;
};

export interface MyInterface {
    name: string;
    age: number;
    sayHello: () => void;
}

export class MyClass implements MyInterface {
    constructor(public name: string, public age: number) { }

    sayHello() {
        console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
    }
}

//-------------------------------------------------------------------------------------

// main.ts

import { myVariable, myFunction, myObject, MyType,
                                            MyInterface, MyClass } from "./utils"

console.log(`myVariable: ${myVariable}`);
console.log(`myFunction: ${myFunction("John")}`);
console.log(`myObject: ${JSON.stringify(myObject)}`);

const myTypeInstance: MyType = { name: "Bob", age: 35 }
console.log(`MyType instance: ${JSON.stringify(myTypeInstance)}`)

const myInterfaceInstance: MyInterface = {
    name: "Alice",
    age: 25,
    sayHello: () => console.log(`Hello, my name is ${myInterfaceInstance.name}`)
};
myInterfaceInstance.sayHello();

const myClassInstance = new MyClass("Dave", 40);
myClassInstance.sayHello();

NOTE : A TypeScript module can contain both type declarations and code, which means that you can define types, interfaces, and other declarations inside a module, and also include executable code that uses those declarations. When you import a module that contains both declarations and executable code, the executable code will be executed every time you import the module.

Example] Below when we execute the main.js file, the executable code inside the utils.js file also gets executed first since we imported from it.


// utils.ts

// Declare a type
type Person = {
    name: string;
    age: number;
};

// Define a function that returns a Person object
function createPerson(name: string, age: number): Person {
    return { name, age };
}

// Export the type and function
export { Person, createPerson };

// Call the function and log the result
const person = createPerson("John", 30);
console.log(person);

//------------------------------------------------------------------------------------

// main.ts

import { Person, createPerson } from "./utils";

const p1:Person = {
    name:"Deepesh",
    age:20
}

const p2  = createPerson("Rohan",20);
console.log(p2)

/*
CMD > tsc && node src/main.js
{ name: 'John', age: 30 }
{ name: 'Rohan', age: 20 }
*/

NOTE : The "module" option in the tsconfig.json file allows you to control what module system the generated JavaScript code uses. There are several module systems that you can choose from such as CommonJS, AMD, UMD, ES6 modules, and SystemJS. By default, TypeScript uses the CommonJS module system for generating code when targeting Node.js, and ES6 modules when targeting modern web browsers.

---------------------------------------------------------------------------------------------------------------

Type Definition / Declaration Files

In TypeScript, type declaration files (".d.ts" files) are files that contain & export type information for libraries or code that don't have built-in TypeScript support. They provide a way to describe the shape of JavaScript code, including its functions, classes, objects, and other entities.

A Type declaration file in TypeScript contains only type definitions and no actual implementation. This means that it defines the structure and types of the code, but not the actual logic of the code. These type declarations provide information about the types of parameters, return types, and other properties of the code.

For example, let's say you have a JavaScript library that provides a function called doSomething(). In order to use this function in TypeScript, you would need to create a type declaration file that describes the function's parameters and return type.


// main.d.ts

// Type declaration for function
declare function Add(x: number, y: number): number;

// Type declaration for object
declare const Person: {
  firstname: string;
  lastname: string;
  age: number;
  address: string;
  isMarrid: boolean;
};

// Type declaration for class
declare class Student {
  fname: string;
  lname: string;
  age: number;
  addNumbers(num1: number, num2: number): number;
  constructor(fname: string, lname: string, age: number);
}

// Export type definitions
export {Add,Person,Student};


Below are some rules to follow when creating type definitions in TypeScript :

  • Type declaration files must have the ".d.ts" file extension to indicate that they contain only type declarations and no executable code. It is short for "declaration file typescript" and indicates that the file is a TypeScript declaration file.
  • If the definition file is for a module, the filename should be the same as the module name with a .d.ts extension. For example, if you have a module named "my-module", the definition file should be named "my-module.d.ts". This way, you don't need to import types from the definition file, as TypeScript will automatically understand to use the type definitions from the declaration file.
  • Always export the type definitions with the "export" keyword to make them available for use outside of the module where they are defined. If you don't need to use a type or interface outside of the module where it is defined, you can omit the export keyword and still use the type or interface within the same module.
  • If TypeScript is unable to find your type definitions, it will try to infer the types on its own and most likely set the any type for the imported JavaScript code. To avoid this, make sure your type definitions are correctly exported and available in the scope where they are needed.

Example] Below we define declaration file for JS module to use it inside TS..


// MathFuncs.js

export function Add(x,y){
    return x+y;
}

export function Sub(x,y){
    return x-y;
}

export function Mul(x,y){
    return x*y;
}

export function Div(x,y){
    return x/y;
}

//------------------------------------------------------------------------------------

// MathFuncs.d.ts

export function Add(x?:number,y:number):number;

export function Sub(x:number,y?:number):number;

export function Mul(x:number,y:number):number;

export function Div(x:number,y?:number):number;

//------------------------------------------------------------------------------------

// main.ts

import { Add, Sub, Mul } from "./MathFuncs";

const sum = Add(10,20)
console.log(sum)

const diff = Sub(10,20)
console.log(diff)

const product = Mul(10,20)
console.log(product)

// COMPILE-ERROR : Parameter 'string' not acceptable
const sum2 = Add("10", 20);

TypeScript comes with number of built-in type declaration files for popular libraries and APIs such as the DOM, Node.js, and many others. These type declaration files are included with the TypeScript compiler and are automatically used to provide type-checking when you write TypeScript code that interacts with these libraries or APIs.

For third-party NPM packages, most come with their own type declaration files. If a package includes its own type declaration file, it will typically be located at path "node_modules/<package>/index.d.ts" at the root of the package. If the package does not include a type declaration file, you may need to create one yourself, or use a community-contributed type declaration file.


The "declare" keyword

The "declare" is an optional keyword often used inside type definition files. In TypeScript, the declare keyword is used to declare the type and shape of variables, functions, classes, and interfaces that are defined in other parts of your code, or in third-party libraries and APIs.

When you use the declare keyword, you're telling TypeScript that the value or entity you're declaring exists, but you're not providing a concrete implementation for it. This is because the implementation may be located in a separate module or library, or it may be dynamically generated at runtime.

Example] Below we define declaration file for JS module to use it inside TS.


// math-functions.js

export function sayMyName(name) {
    console.log(`Hello, I am ${name}`)
}

export function Add(x, y) {
    return x + y;
}

export function Sub(x, y) {
    return x - y;
}

//------------------------------------------------------------------------------------

// math-functions.d.ts

declare function Add(x: number, y: number): number;
declare function Sub(x: number, y: number): number;

declare type Person = {
    fname: string,
    lname: string,
    country: string,
    age: number
};

declare function sayMyName(name: string): void;

export { Add, Sub, Person, sayMyName }

//------------------------------------------------------------------------------------

// main.ts

import { Add, Sub, sayMyName, Person } from "./math-functions";

const person: Person = {
    fname: 'Deepesh',
    lname: 'Mhatre',
    country: 'India',
    age: 20
};

const sum = Add(100, 200);
const sum = Add(100, "200"); // Error : Number expected provided String
const diff = Sub(1000, 200);
console.log(sum, diff)
console.log(person)
sayMyName("Deepesh")


/*
CMD> tsc && node src/main.js
300 800
{ fname: 'Deepesh', lname: 'Mhatre', country: 'India', age: 20 }
Hello, I am Deepesh
*/


DefinitelyTyped

Most of the Javascript libraries today support Typescript or atleast come with their own declaration files which we can use to add in our project. Some packages don't support Typescript or don't come with a type definition file, in such cases we can either write our own type definition file or find one and include it in our project.

To help TypeScript developers use such packages, "Definitely Typed" is a project that provides a central repository of TypeScript definition files for commonly used npm packages, it provides a searchable directory of available type declaration files that you can use in your TypeScript projects.


// Syntax for installing type definition files for any package:
// npm install --save <package-name> @types/<package-name>

// Install React and its type definition files
npm install --save react @types/react

// Install React Router and its type definition files
npm install --save react-router @types/react-router

// Install Axios and its type definition files
npm install --save axios @types/axios

// Install Express and its type definition files
npm install --save express @types/express

// Install Lodash and its type definition files
npm install --save lodash @types/lodash

// Install Moment.js and its type definition files
npm install --save moment @types/moment

// Install Redux and its type definition files
npm install --save redux @types/redux

// Install Socket.io and its type definition files
npm install --save socket.io @types/socket.io

// Install Sequelize and its type definition files
npm install --save sequelize @types/sequelize

// Install Firebase and its type definition files
npm install --save firebase @types/firebase

NOTE : For the most part, type declaration packages should always have the same name as the package name on npm, but prefixed with @types/, but if you need, you can use the npm package search to find the package for your favorite library.

NOTE : Many of these popular packages, such as React, Redux, and Moment.js, already include their own type definitions, which means you can use them without the need to install any additional @types packages. However, installing @types packages can still be helpful in cases where the package does not provide its own type definitions, or if you need to use a version of the package that doesn't have type definitions included.

---------------------------------------------------------------------------------------------------------------

Typescript/JS Interoperability

TypeScript and JavaScript have full interoperability, meaning you can use TypeScript code in JavaScript projects and vice versa. TypeScript is a superset of JavaScript, which means that any valid JavaScript code is also valid TypeScript code.

NOTE : Whether we import Typescript code into JS, or Javascript code into TS we always perform compilation using TSC and only execute the compiled code.


Import Typescript into JS files

We can use TypeScript code in JavaScript projects by simply compiling the TypeScript code into JavaScript. The generated JavaScript code can be used in any JavaScript environment, and it will work the same way as regular JavaScript code. Importing TypeScript modules into JavaScript files does not require any special configuration other than compiling the TypeScript code into JavaScript.

Example] Below we import Typescript functions into JS file, the Typescript code will be compiled to JS with TSC and the generated JS is executed


// utils.ts

type Person = {
    fname:string,
    lname:string,
    age:number,
    isMarried:boolean,
}

export const p1:Person = {
    fname:"Amit",
    lname:"Singh",
    age:20,
    isMarried:false,
}

export function Add(num1:number, num2:number):number{
    return num1+num2;
 }

 export function Sub(num1:number, num2:number){
    return num1-num2;
 }

 export function Multiply(num1:number, num2:number){
    return num1*num2;
 }

//------------------------------------------------------------------------------------

// main.js

import { Add, Sub, Multiply, p1 } from "./utils";

let sum = Add(10,20.33)
let diff = Sub(55,44)
let result = Multiply(15,12)

console.log(sum, diff, result)
console.log(p1.fname, p1.lname, p1.age)

/*
CMD> tsc && node src/main.js
30.33 11 180
Amit Singh 20
*/


Import Javascript into TS files

Importing regular Javascript modules inside a Typescript file require some configuration, since the JS modules don't have type information which can cause errors or warnings when you try to use it in your TypeScript code. Below are some approaches for importing JS modules into TypeScript files :


1] Enable the "allowJS" option

If you are working with JavaScript files in your TypeScript project and want to be able to import them directly, you can enable the 'allowJs' option in your tsconfig.json file. This allows the TypeScript compiler to include JavaScript files in the compilation process along with TypeScript files and perform type checking on them.



{
    "compilerOptions": {
      "allowJs": true,
      "module": "commonjs",
      "target": "es5",
      "sourceMap": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
  }
 

By default if we have any JS files in our Typescript project, they are not included into the compiled code generated by the Typescript compiler (TSC). So if we are importing JS modules into TS files, we need to enable the "allowJS" option, otherwise we may get the "module not found" error at runtime.


// utils.js

export function Add(num1, num2){
    return num1+num2;
 }

 export function Sub(num1, num2){
    return num1-num2;
 }

 export function Multiply(num1, num2){
    return num1*num2;
 }

//------------------------------------------------------------------------------------

// main.ts

import { Add, Sub, Multiply } from "./utils";

let sum = Add(10,20.33)
let diff = Sub(55,44)
let result = Multiply(15,12)

console.log(sum, diff, result)


/*
CMD> tsc && node src/main.js
30.33 11 180
*/


NOTE : When the "allowJS=true", Typescript will try to infer the types fo each export inside the JS module. The type is set to "any" if it's unable to infer the type based on input parameters and return types.


2] Use Type Declaration File

Most third-party packages come with their own type declaration files or if not we can install one from the DefinitelyTyped project. InCase a definition file is not available we can create one on our own.


// utils.js

export function Add(num1, num2){
    return num1+num2;
 }

 export function Sub(num1, num2){
    return num1-num2;
 }

 export function Multiply(num1, num2){
    return num1*num2;
 }

//------------------------------------------------------------------------------------

// utils.ts

export function Add(num1:number, num2:number):number;

export function Sub(num1:number, num2:number):number;

export function Multiply(num1:number, num2:number):number;

//------------------------------------------------------------------------------------

// main.ts

import { Add, Sub, Multiply } from "./utils";

let sum = Add(10,20.33)
let diff = Sub(55,44)
let result = Multiply(15,12)

console.log(sum, diff, result)

/*
CMD> tsc && node src/main.js
30.33 11 180
*/

NOTE : If we are importing our own JS modules into TS file, we must also set the "allowJS=true" to include those JS files into the compiled code.

---------------------------------------------------------------------------------------------------------------

Literal Types

In Typescript, a literal type is basically a type with specific literal value attached to it. Literal types can be used to enforce that a value must be of a specific type and a specific value. 


type Age = 42;
let age: Age = 42;   // ok
let age: Age = 43;  // error

type Country = "India"
let country: Country = 'india' // ok
let country: Country = 'USA'  // error

function myFunc(x:Age){ return x }
myFunc(42)  // ok
myFunc(12)  // error

Literal types can also be combined with union types to create more complex types. For example, a variable of type "Alice" | "Bob" can have the value "Alice" or "Bob", but not any other string value.

Example] Below we create literal types and use them with functions and variables.


// Literal types
let name: "Alice" = "Alice";
let age: 42 = 42;
let isAlive: true = true;

//------------------------------------------------------------------------------------

// Union types with literal types
let nameOrAge: "Alice" | 42 = "Alice";
nameOrAge = 42;
nameOrAge = "Bob"; // Error

// Function with parameter of literal type
function greet(name: "Alice") {
  console.log(`Hello, ${name}!`);
}

greet("Alice");
greet("Bob"); // Error

//------------------------------------------------------------------------------------

// Object with property of literal type
interface Person {
  name: "Alice";
  age: 42;
}

let person1: Person = {
  name: "Alice",
  age: 42,
};

let person2: Person = { name: "Bob", age: 30 }; // Error


KeyOf Operator

The keyof operator in TypeScript is a type operator that produces a union type of all the keys of a given type. It allows you to extract the keys of an object or a type, and use them as a type themselves.


interface Person {
    name: string;
    age: number;
    email: string;
}

//This type can only store one of these keys
type PersonKeys = keyof Person; // "name" | "age" | "email"

const key: PersonKeys = "name"; // OK

// Error: "phone" is not assignable to type "name" | "age" | "email"
const anotherKey: PersonKeys = "phone";

//------------------------------------------------------------------------------------
function getKey(key:PersonKeys):string{
    return "Key Received : "+key;
}

const i = getKey("name");
const j = getKey("age");
// ERROR : Argument of type '"deepesh"' is not assignable
const k = getKey("deepesh");

The keyof operator in TypeScript is closely related to literal types, we can use it to create types which can only store values from the set of extracted keys, passing any other values will result in an compile-time error.

Example] Below extract keys from interfces & types to create new union types.


interface Person {
    name: string;
    age: number;
    email: string;
}

interface somePerson {
    name: "Deepesh";
    age: 20;
    email: "deepesh@mail.com";
}

type Animal = {
    name: string;
    age: number;
    canFly: boolean;
}

type Horse = {
    name: "Bazigar";
    age: 2.5;
    canFly: false;
}


type personKeys = keyof Person              // "name" | "age" | "email"
type somePersonKeys = keyof somePerson     // "name" | "age" | "email"
type animalKeys = keyof Animal            // "name" | "age" | canFly"
type horseKeys = keyof Horse             // "name" | "age" | "canFly"


// create literal types
const someKey1:personKeys = "name"
const someKey2:personKeys = "age"
const someKey3:personKeys = "Deepesh123" // ERROR : '"Deepesh123"' is not assignable
const someKey4:animalKeys = "canFly"
const someKey5:animalKeys = "canFly123" // ERROR : '"canFly123"' is not assignable



Template Literal Types

Template literal types in TypeScript are a way to manipulate string values as types. They allow you to create a type based on the result of string manipulation or concatenation. Template literal types are created using the backtick (``) character and string manipulation expressions within the type.


// Type Name can only take strings that start with 'Mr.'
type Name = `Mr. ${string}`
let name1: Name = `Mr. Smith`;  // ok
let name2: Name = `Mrs. Smith`;  // error

// Type Age can only take strings that start 'My age is'
type Age = `My age is ${number}`
let age1: Age = `My age is 20`;  // ok
let age2: Age = `His age is not 20`;  // error


---------------------------------------------------------------------------------------------------------------

Conditional Types

In TypeScript, conditional types allow you to express types that depend on other types. They provide a way to create more flexible and dynamic types that can change based on the values of other types.

The only condition that we can check in a conditional type is whether a given type extends (or is a child of) some other type. In TypeScript, the 'extends' keyword is used to check whether one type is assignable to another type. This allows us to create conditional types that can assign different types based on whether a given type extends another type.


 // If a given type 'SomeType' extends another given type 'OtherType', then
 // 'ConditionalType' is 'TrueType', otherwise it is 'FalseType'.

 // SYNTAX :
 type ConditionalType = SomeType extends OtherType ? TrueType : FalseType

 type Extends<T, U> = T extends U ? T : U;
 type A = Extends<string, number>;  // type A is 'number'
 type B = Extends<boolean, string>;  // type B is 'string'

//------------------------------------------------------------------------------------

interface Animal {
    live(): void;
}

interface Dog extends Animal {
    woof(): void;
}

interface Cow {
    woof():void
}

type Example1 = Dog extends Animal ? number : string;  // number
type Example2 = Cow extends Animal ? number : string;  // string

While the extends keyword is the only way to express a condition in a conditional type, it can be used in conjunction with other type operators such as union types, intersection types, and keyof types to create more complex and powerful type definitions.


// Using extends with union types

type Animal = {
    species: string;
    name: string;
};

type Dog = Animal & {
    breed: string;
};

type Cat = Animal & {
    breed: string;
};

type Pet = Dog | Cat;

type IsDog<T> = T extends Dog ? true : false;

type PetIsDog = IsDog<Pet>; // returns true

//------------------------------------------------------------------------------------

// Using extends with keyof types

type Person = {
    name: string;
    age: number;
    email: string;
};

type PersonKeys = keyof Person;

type IsPersonKey<T> = T extends PersonKeys ? true : false;

type MyKey: IsPersonKey<"name">; // returns true
type MyOtherKey: IsPersonKey<"address">; // returns false


---------------------------------------------------------------------------------------------------------------

Index Signatures

An index signature is basically a rule that if certain type of key exist then it can only hold a certain type of value. It allows you to define an object type that can have any number of properties with different types of keys and values, but if a key has an certain type, then it can only hold a certain type of value, as specified by the index signature. This makes it possible to create flexible object types that can still enforce type safety for their properties.

The syntax for an index signature in Typescript is the following :


// Syntax
[keyType: Type]: valueType

// Examples
[key1: string]: number,  // index signature for string keys with number values
[index2: number]: string, // index signature for number keys with string values
[key: symbol]: boolean, // index signature for symbol keys with boolean values

NOTE : The 'keyType' in the index signature syntax is just a placeholder name, it can be any valid name. The 'keyType' is the type of the key, and 'valueType' is the type of the value associated with that key. The key type can be any primitive type or a union of primitive types, while the value type can be any valid TypeScript type, including other object types or interfaces.

Example] Below we enforce that keys of string type can only hold number or string type values, if we give any other type of value we'll get an error.


type Person = {
    [key: string]: number | string
  }
 
const p1: Person = {
    firstname: "Deepesh",
    lastname: "Shrestha",
    age: 30,
    salary: 5000,
    123:"Hello",
    obj:{"name":"Deepesh"} // Error: Only string|number values allowed for string keys
    isMarried: false // Error: Only string|numver values allowed for string keys
  }
 

Example] Below we define index signatures and apply them to different objects.


// String type keys can only hold number types.
type NumberObject = {
    [key: string]: number
}

const obj: NumberObject = {
    "key1": 1,
    "key2": 2,
    "key3": 3,
    "key4": "Hello", // Error
    "key5": [1, 2, 3] // Error
}

//------------------------------------------------------------------------------------

// Number type keys can only hold string types.
type StringObject = {
    [key: number]: string
}

const obj: StringObject = {
    1: "value1",
    2: "value2",
    3: "value3",
    firstname: "Deepesh",
    4: 777  // Error
}

//------------------------------------------------------------------------------------

// String & Number type keys can only hold boolean or string types
type MixedObject = {
    [key: string | number]: boolean | string
}

const obj: MixedObject = {
    1: "value1",
    "key2": true,
    3: "value3",
    "key4": false
}

//------------------------------------------------------------------------------------

// String keys can only holds Car type.
type Car = {
    brand: string,
    type: string,
    year: number,
    price: number
}

type Vehicles = {
    [key: string]: Car
}

const myCars: Vehicles = {
    "car1": {
        brand: "Toyota",
        type: "Sports",
        year: 1999,
        price: 35000
    },
    "car2": {
        brand: "Ferrari",
        type: "Sports",
        year: 2015,
        price: 150000
    },
    "car3": {
        brand: "Range Rover",
        type: "SUV",
        year: 2001,
        price: 55000
    },
    "car4": "Hello" // Type 'string' is not assignable to type 'Car'
}

NOTE : Index signatures work for interfaces and type aliases, but any object can only have one index signature.


Mapped Types

In TypeScript, mapped types are a powerful feature that allows you to transform an existing type into a new type by applying a transformation to all properties. Mapped types are based on a set of index signatures that define how the properties of the input type should be transformed to create the output type.

The Utility types like Partial, ReadOnly and various other types are based on mapped types, which allows them to map properties to various transformations.

Mapped types are commonly made to be of generic type so that they can work with a variety of different input types and are based on index signatures. In Mapped types, we iterate over all the keys of a given type or interface using the "in" operator, which is often used with "keyof" operator to extract a union of keys from given type.


// syntax for mapped types

type newType = {
    [key in "key1" | "key2" | "key3"]: valueType
}

type newType = {
    [key in keyof oldType]: valueType
}

Example] Below we define various mapped types to transform any given type.


// Making all properties of an interface optional
type Optional<T> = { [P in keyof T]?: T[P] };

// Making all properties of an interface required and not nullable
type NotNullable<T> = { [P in keyof T]-?: NonNullable<T[P]> };

// Making all properties of an interface required and nullable
type Nullable<T> = { [P in keyof T]-?: T[P] | null };

// Making all properties of an interface readonly and not nullable
type ReadonlyNotNullable<T> = { readonly [P in keyof T]: NonNullable<T[P]> };

// Making all properties of an interface readonly and nullable
type ReadonlyNullable<T> = { readonly [P in keyof T]: T[P] | null };

// Making all properties of an interface optional and readonly
type ReadonlyOptional<T> = { readonly [P in keyof T]?: T[P] };

// Making all properties of an interface required, but with a type constraint
type RequiredOfType<T, U> = { [P in keyof T]-?: T[P] extends U ? T[P] : never };

// Making all properties of an interface optional, but with a type constraint
type OptionalOfType<T, U> = { [P in keyof T]?: T[P] extends U ? T[P] : never };

Example] Below we define mapped type to transform all properties to boolean.


type Animal = {
    name: string;
    age: number;
    canFly: boolean;
}

let Horse: Animal = {
    name: "Bazigar",
    age: 2.5,
    canFly: false
}

//-------------------------------------------------------------------------------------

// Transform all Animal properties to hold only boolean values
type BoolAnimal = {
    [key in keyof Animal]: boolean
}


let BoolAnimal1: BoolAnimal = {
    name: true,
    age: false,
    canFly: true,
}

let BoolAnimal2: BoolAnimal = {
    name: "Baazzigar",  // Error : Type 'string' is not assignable to type 'boolean'
    age: false,
    canFly: true,
}

//-------------------------------------------------------------------------------------

// Transform all properties of given Type to hold only boolean values
type All2Boolean<T> = {
    [property in keyof T]: boolean
}

let BoolAnimal3: All2Boolean<Animal> = {
    name: true,
    age: false,
    canFly: true,
}

let BoolAnimal4: All2Boolean<Animal> = {
    name: "Baazzigar",  // Error : Type 'string' is not assignable to type 'boolean'
    age: false,
    canFly: true,
}

Example] Below we define mapped type with and without keyOf operator.


// Mapped type with keyof

interface Person {
    name: string;
    age: number;
    address: string;
  }
 
  type OptionalPerson = { [P in keyof Person]?: Person[P] };
 
  const person: OptionalPerson = { name: "Alice", age: 30 };
 
  console.log(person); // { name: "Alice", age: 30, address: undefined }
 
//------------------------------------------------------------------------------------

// Mapped type without keyof

interface Person {
    name: string;
    age: number;
    address: string;
  }
 
  type OptionalPerson = { [P in "name" | "age" | "address"]?: Person[P] };
 
  const person: OptionalPerson = { name: "Alice", age: 30 };
 
  console.log(person); // { name: "Alice", age: 30, address: undefined }
 


---------------------------------------------------------------------------------------------------------------

Typescript Namespaces

In TypeScript, a namespace is a way to organize code into a logical group or module. A namespace is declared using the namespace keyword, followed by the name of the namespace and a pair of curly braces {} that contain the code to be grouped together.

Namespaces can be used to avoid naming conflicts between different parts of a program, as all the code within a namespace is effectively encapsulated and isolated from other code.


// main.ts

namespace MyNamespace1 {
    export function doSomething() {
        console.log("Doing something in Namespace1...");
    }
}

namespace MyNamespace2 {
    export function doSomething() {
        console.log("Doing something in Namespace2...");
    }
}

MyNamespace1.doSomething();
MyNamespace2.doSomething();

Example] Below we define multiple namespaces and acess them in different file.


// utils.ts

export namespace MyNamespace1 {
    export function doSomething() {
        console.log("Doing something in Namespace1...");
    }
}

export namespace MyNamespace2 {
    export function doSomething() {
        console.log("Doing something in Namespace2...");
    }
}

//------------------------------------------------------------------------------------

// main.ts

import { MyNamespace1,MyNamespace2 } from "./utils";

MyNamespace1.doSomething();
MyNamespace2.doSomething();


---------------------------------------------------------------------------------------------------------------

Typescript Decorators

TypeScript decorators are a feature introduced in TypeScript that allow you to add metadata and modify the behavior of classes, methods, properties, and parameters at runtime. They provide a way to extend and modify the functionality of existing code without directly modifying the code itself.

Decorators are executed during the class declaration phase, specifically when the class is defined. They are invoked immediately after the class declaration and before any instances of the class are created.

There are different types of Typescript decorators, some are as followed :

  • Class Decorators : Applied to classes, they modify or extend class behavior or provide additional metadata.
  • Method Decorators : Applied to methods, they modify or observe method behavior, provide metadata, or perform actions before or after method execution.
  • Property Decorators : Applied to properties, they modify or observe property behavior, provide metadata, or perform actions related to the property.
  • Parameter Decorators : Applied to method or constructor parameters, they modify or observe parameter behavior, provide metadata, or perform actions related to the parameter.

NOTE : Decorators are essentially functions that can be attached to classes, methods, properties, or parameters using the "@" symbol followed by the decorator name.


// CLASS DECORATOR

function logClass(target: any) {
    console.log(`Class ${target.name} is defined.`);
}

@logClass
class MyClass {
    // Class implementation
}

//-------------------------------------------------------------------------------------

// METHOD DECORATOR

function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
        console.log(`Calling method ${key}`);
        const result = originalMethod.apply(this, args);
        return result;
    };

    return descriptor;
}

class Example {
    @logMethod
    greet(name: string) {
        console.log(`Hello, ${name}!`);
    }
}

const instance = new Example();
instance.greet("John"); // Output: "Calling method greet" and "Hello, John!"



// PROPERTY DECORATOR

function logProperty(target: any, key: string) {
    let value = target[key];
 
    const getter = function () {
      console.log(`Getting value of property ${key}`);
      return value;
    };
 
    const setter = function (newValue: any) {
      console.log(`Setting value of property ${key}`);
      value = newValue;
    };
 
    Object.defineProperty(target, key, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  }
 
  class Example {
    @logProperty
    message: string = "Hello";
  }
 
  const instance = new Example();
  console.log(instance.message); // Output: "Getting value of property message" and "Hello"
  instance.message = "Hi"; // Output: "Setting value of property message"
  console.log(instance.message); // Output: "Getting value of property message" and "Hi"
 
//----------------------------------------------------------------------------------------------

  // PARAMETER DECORATOR
 
  function logParameter(target: any, key: string, parameterIndex: number) {
    console.log(`Parameter ${parameterIndex} of method ${key} is decorated.`);
  }
 
  class Example {
    greet(@logParameter name: string) {
      console.log(`Hello, ${name}!`);
    }
  }
 
  const instance = new Example();
  instance.greet("John"); // Output: "Parameter 0 of method greet is decorated." and "Hello, John!"

NOTE : Different types of decorator functions in TypeScript receive different types of arguments or inputs based on the element they are applied to.

Class decorators receive a single argument, which is the constructor function of the class being decorated. The type of this argument is Function or typeof ClassName , whereas Method decorators receive three different arguments.

---------------------------------------------------------------------------------------------------------------



Comments

Popular posts from this blog

React Js + React-Redux (part-2)

React Js + CSS Styling + React Router (part-1)

ViteJS (Module Bundlers, Build Tools)