Tuesday, July 14, 2026

TypeScript

It is a superset of JavaScript. TS builds on top of Javascript.

The main goals of TypeScript are:

  • Introduce optional types to JavaScript.
  • Implement planned features of future JavaScript, a.k.a. ECMAScript Next or ES Next to the current JavaScript.
TypeScript improves your productivity while helping avoid bugs.

Terminal to compile the app.ts file
tsc app.ts

Notes from : www.typescripttutorial.net

If installed the tsx module
tsx app.ts
  • JavaScript is dynamically typed, providing flexibility but also leading to many problems.
  • TypeScript adds an optional type system to JavaScript to solve these problems.
Typescript shows error during the compile time if the syntax does not matches.

  • a type is a label that describes the different properties and methods that a value has
  • every value has a type.
  • In TypeScript, every value is associated with a type.
  • TypeScript compiler uses types to analyze your code for hunting bugs and errors.
Once an identifier is annotated with a type, it can be used as that type only. If the identifier is used as a different type, the TypeScript compiler will issue an error.
Type is assigned to values not to variables.

TypeScript inherits the built-in types from JavaScript. TypeScript types are categorized into:

  • Primitive types.
  • Object types.

Primitive types

The following illustrates the primitive types in TypeScript:

NameDescription
stringRepresent text data.
numberRepresent numeric values.
booleanHave true and false values.
nullHave one value: null.
undefinedHave one value: undefined. It is the default value of an uninitialized variable.
symbolRepresent a unique constant value.

Object types

Object types are functions, arrays, classes, etc. Later, you’ll learn how to create custom object types.

let variableName: type;
let variableName: type = value;
const constantName: type = value;
let name: string = 'John';
let age: number = 25;
let active: boolean = true;
Type annotation examples
Arrays
let names: string[] = ['John', 'Jane', 'Peter', 'David', 'Mary'];
Objects
let person: {
  name: string;
  age: number;
};

person = {
  name: 'John',
  age: 25,
}; // valid

Function arguments & return types


let greeting : (name: string) => string;
greeting = function (name: string) {
    return `Hi ${name}`;
};

Type inference


let counter = 0; // Code language: JavaScript

It is equivalent to the following statement:

let counter: number = 0; / Code language: TypeScript

Infer type by self

If you add a string to the items array, TypeScript will infer the type for the items as an array of numbers and strings: (number | string)[]

Best common type algorithm

let items = [1, 2, 3, 'Cheese'];

Contextual typing

infers event as click

document.addEventListener('click', function (event) {
    console.log(event.button);
});

// if changed to scroll then it will show error

So, when do you use type inference and type annotations?

In practice, you should always use the type inference as much as possible. You use the type annotation in the following cases:

  • When you declare a variable and assign it a value later.
  • When you want a variable that can’t be inferred.
  • When a function returns the any type, you need to clarify the value.

TypeScript numbers

All numbers in TypeScript are either floating-point values that get the number type or big integers that get the bigint type.

JavaScript has the Number type (with the letter N in uppercase) that refers to the non-primitive boxed object. You should not use this Number type as much as possible in TypeScript. Avoid using the Number type as much as possible.

let price: number;
price = 9;

String type

TypeScript uses double quotes (") or single quotes (') to surround string literals. 

To create a multi-line string using the backtick (`) .

let description = `This TypeScript string can
span multiple
lines
`;

let firstName: string = `John`;
let title: string = `Web Developer`;
let profile: string = `I'm ${firstName}.
I'm a ${title}`;

console.log(profile);

Boolean type

The TypeScript boolean type has two values: true and false. The boolean type is one of the primitive types in TypeScript.

let pending: boolean;
pending = true;
// after a while
// ..
pending = false;

// NOT operator
const pending: boolean = true;
const notPending = !pending; // false
console.log(result); // false

const hasError: boolean = false;
const completed: boolean = true;

// Logical AND operator
let result = completed && hasError;
console.log(result); // false

// Logical OR operator
result = completed || hasError;
console.log(result); // true

JavaScript has the Boolean type that refers to the non-primitive boxed object. The Boolean type has the letter B in uppercase, which is different from the boolean type.

It’s a good practice to avoid using the Boolean type.

Object Type

The TypeScript object type represents all values that are not in primitive types.

The following are primitive types in TypeScript:
number, bigint, string, boolean, null, undefined, symbol.

How to declare a variable that holds an object.

let employee: object;

employee = {
    firstName: 'John',
    lastName: 'Doe',
    age: 25,
    jobTitle: 'Web Developer'
};

console.log(employee);

// Output
{
  firstName: 'John',      
  lastName: 'Doe',
  age: 25,
  jobTitle: 'Web Developer'
}

employee = "Jane"; // Error TS2322: Type '"Jane"' is not assignable to type 'object'.

console.log(employee.hireDate);
// error TS2339: Property 'hireDate' does not exist on type 'object'.

To explicitly specify properties of the employee object

let employee: {
    firstName: string;
    lastName: string;
    age: number;
    jobTitle: string;
} = {
    firstName: 'John',
    lastName: 'Doe',
    age: 25,
    jobTitle: 'Web Developer'
};

object vs. Object

The object type represents all non-primitive values while the Object type describes the functionality of all objects.

The Object type has the toString() and valueOf() methods that can be accessible by any object.

The empty type {}

TypeScript has another type called empty type denoted by {} , which is quite similar to the object type.

let vacant: {};
vacant.firstName = 'John'; // compile time error

let vacant: {} = {};
console.log(vacant.toString()); // can access all properties and methods declared on
the Object type

Understanding TypeScript Empty Object Type — xjavascript.com

Arrays 

let skills: string[] = [];
skills[0] = "Problem Solving";
skills[1] = "Programming";
skills.push('Software Design');

skills.push(100);// Error

let skill = skills[0];
console.log(typeof(skill)); // output: string

Storing Values of mixed types


let scores : (string | number)[];
scores = ['Programming', 5, 'Software Design', 4];

Unknown Type

The unknown type is like any type but more restrictive.

Use the unknown type to handle data coming from external sources and requires validation before use.

unknown type can hold a value that is not known upfront but requires type checking.

let result: unknown;
result = [1,2,3];

const total = result.reduce((a: number, b:number ) => a + b, 0); // Error
console.log(total);

// With type assertion is correct.
const total = (result as number[]).reduce((a: number, b: number) => a + b, 0);
console.log(total); // 6

Featureanyunknown
Type SafetyNo type-safetyEnforces type safety
OperationsOperations can be performed without checksOperations cannot be performed without type assertion (narrowing type)
Use casesUseful for dynamic values but unsafe.Useful for dynamic values and safe because it requires validation before use.
Type CheckingTypeScript compiler does not perform a type checking on an any variable.TypeScript compiler enforces a type checking on an unknown variable.
Common ScenariosUsed for migrating JavaScript codebase to TypeScript.Used when handling data from external sources (API calls, databases, ..) where type validation is necessary.

const fetchData = async (url: string): Promise<unknown> => {
    const response = await fetch(url);
    return await response.json();
};

const showPosts = async () => {
    const url = 'https://jsonplaceholder.typicode.com/posts';
    try {
        const posts = await fetchData(url); // unknown type

        (
            posts as { userId: number; id: number; title: string; body: string }[]
        ).map((post) => console.log(post.title));
    } catch (err) {
        console.log(err);
    }
};

showPosts();

let valueAny: any = "Hello";
let valueUnknown: unknown = "Hello";

// ✅ Works — but unsafe (no checks)
valueAny.trim(); // No error, but could fail at runtime

// ❌ Error — must check type first
// valueUnknown.trim(); // Compile-time error

// ✅ Safe usage with type check
if (typeof valueUnknown === "string") {
    console.log(valueUnknown.trim());
}

function parseJson(jsonString: string): unknown {
    try {
        return JSON.parse(jsonString);
    } catch {
        return null;
    }
}

const data = parseJson('{"name": "Alice"}');

// ❌ Direct property access not allowed
// console.log(data.name);

// ✅ Safe usage with type guard
if (typeof data === "object" && data !== null && "name" in data) {
    console.log((data as { name: string }).name);
}

Tuples

Tuple works like an array.

The number of elements in the tuple is fixed.

The types of elements are known, and need not be the same.

you can use a tuple to represent a value as a pair of a string and a number

let skill: [string, number];
skill = ['Programming', 5]; // correct

The order of values in a tuple is important.
let skill: [string, number];
skill = [5, 'Programming']; // wrong

It’s a good practice to use tuples with data that are related to each other in a specific order. e.g. (r,g,b)
let color: [number, number, number] = [255, 0, 0];

Optional tuple elements
let bgColor, headerColor: [number, number, number, number?];
bgColor = [0, 255, 255, 0.5];
headerColor = [0, 255, 255];

Enum

An enum is a group of named constant values. Enum stands for enumerated type.

// Syntax
enum name {constant1, constant2, ...};

The generated object also has number keys with string values representing the named constants.

You can pass a number into the function that accepts an enum. In other words, an enum member is both a number and a defined constant.

enum Month { Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec };

function isItSummer(month: Month) {
  let isSummer: boolean;
  switch (month) {
    case Month.Jun:
    case Month.Jul:
    case Month.Aug:
      isSummer = true;
      break;
    default:
      isSummer = false;
      break;
  }
  return isSummer;
}

console.log(isItSummer(Month.Jun)); // true
console.log(isItSummer(6)); // true

The values of Jan is 0, Feb is 1 and so on.

Specifying enum members numbers

enum Month { Jan = 1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec };

If we specify a member a number then consecutive members will add +1 to it and all will be affected.

You should use an enum when you:

  • Have a small set of closely related fixed values.
  • And these values are known at compile time.
enum ApprovalStatus {
    draft,
    submitted,
    approved,
    rejected
};

const request =  {
    id: 1,
    status: ApprovalStatus.approved,
    description: 'Please approve this request'
};

if(request.status === ApprovalStatus.approved) {
    // send an email
    console.log('Send email to the Applicant...');
}

Any

Sometimes, you may need to store a value in a variable. But you don’t know its type when writing the program.
And the unknown value may come from a third-party API or user input.
you want to opt out of the type checking and allow the value to pass through the compile-time check.

let result: any;

result = 1;
console.log(result); // 1

result = 'Hello';
console.log(result); // Hello

result = [1, 2, 3];
const total = result.reduce((a: number, b: number) => a + b, 0);
console.log(total); // 6

// json may come from a third-party API
const json = `{"latitude": 10.11, "longitude":12.12}`;

// parse JSON to find location
const currentLocation = JSON.parse(json);
console.log(currentLocation);

// Output is json - {"latitude": 10.11, "longitude":12.12}

console.log(currentLocation.x); // undefined


The TypeScript compiler doesn’t complain or issue any errors, if it is undefined.

If you declare a variable with the object type, you can also assign any value to it. However, you cannot call a method on it even if the method exists. For example:

let result: object;
result = 10.123;
result.toFixed(); // Error

let result: any;
result = 10.123;
console.log(result.toFixed());
result.willExist(); // error on execution not on compile

Void

The void type denotes the absence of having any type at all. Typically, you use the void type as the return type of functions that do not return a value.

Notice that if you use the void type for a variable, you can only assign undefined to that variable.

let useless: void = undefined;
useless = 1; // error

If the --strictNullChecks flag is not specified, you can assign the useless to null.

useless = null; // OK if --strictNullChecks is not specified

Union ( | )

The union type allows you to combine multiple types into one type.
Means it can take any of them.

let result: number | string;
result = 10; // OK
result = 'Hi'; // also OK
result = false; // a boolean value, not OK


function add(a: number | string, b: number | string) :  number | string {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    }
    if (typeof a === 'string' && typeof b === 'string') {
        return a.concat(b);
    }
    throw new Error('Parameters must be numbers or strings');
}

Type Aliases

A type alias allows you to create a new name for an existing type.

Type aliases can be useful for:

  • Simplifying complex types.
  • Making code more readable.
  • Creating reusable types that can be used in many places in the codebase.
type alias = existingType;   // Syntax

The existing type can be any valid TypeScript type including primitive type, object type, union type, intersection type, and function type.

1) Primitive types


type Name: string;
let firstName: Name;
let lastName: Name;

2) Object types {}


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

let person: Person = {
  name: 'John',
  age: 25
};

3) Union Types ( | )


type alphanumeric = string | number;

let input: alphanumeric;
input = 100; // valid
input = 'Hi'; // valid
input = false; // Compiler error

4) Intersection Types  ( & )


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

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

type Candidate = Personal & Contact;

let candidate: Candidate = {
  name: "Joe",
  age: 25,
  email: "joe@example.com",
  phone: "(408)-123-4567"
};

String Literal

A TypeScript string literal type defines a type that accepts specified string literal.

Use the string literal types with union types and type aliases to define types that accept a finite set of string literals.

The string literal type is useful to limit a possible string value that a variable can store.

let click: 'click';
// it can accept only value 'click'

click = 'click'; // valid
// 'click' can be reassigned

click = 'dblclick'; // compiler error

The string literal types can combine nicely with the union types to define a finite set of string literal values for a variable:

let mouseEvent: 'click' | 'dblclick' | 'mouseup' | 'mousedown';
mouseEvent = 'click'; // valid
mouseEvent = 'dblclick'; // valid
mouseEvent = 'mouseup'; // valid
mouseEvent = 'mousedown'; // valid
mouseEvent = 'mouseover'; // compiler error

If you use the string literal types in multiple places, they will be verbose.

To avoid this, you can use the type aliases. For example:

type MyMouseEvent = 'click' | 'dblclick' | 'mouseup' | 'mousedown';
let mouseEvent: MyMouseEvent;
mouseEvent = 'click'; // valid
mouseEvent = 'dblclick'; // valid
mouseEvent = 'mouseup'; // valid
mouseEvent = 'mousedown'; // valid
mouseEvent = 'mouseover'; // compiler error

let anotherEvent: MyMouseEvent;

Never type

TypeScript never type to represent a value that never occurs.

The never type is a type that holds no value. It is like an empty set.

Since a never type does not hold any value, you cannot assign a value to a variable with the never type.

let empty: never = 'hello';
// Type 'string' is not assignable to type 'never'

Since the never type has zero value, you can use it to denote an impossibility in the type system.

type Alphanumeric = string & number; // never                                                                    

If you have strictNullChecks and strict mode off, TypeScript may allow assignment with only a warning or no error.

In strict mode, this code will give
Type 'string' is not assignable to type 'never'.

Therefore, the TypeScript compiler infers the type of Alphanumeric as never.

This is because string and number are mutually exclusive. In other words, a value cannot be both a string and a number simultaneously.

Typically, you use the never type to represent the return type of a function that never returns the control to the caller.

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

If you have a function that contains an indefinite loop, its return type should be never.

type Role = 'admin' | 'user';

const authorize = (role: Role): string => {
  switch (role) {
    case 'admin':
      return 'You can do anything';
    case 'user':
      return 'You can do something';
    default:
      // never reach here until we add a new role
      const _unreachable: never = role;
      throw new Error(`Invalid role: ${_unreachable}`);
  }
};

console.log(authorize('admin'));
//----------------------- OR ------------------------------

type Role = 'admin' | 'user' | 'guest';

const unknownRole = (role: never): never => {
  throw new Error(`Invalid role: ${role}`);
};

const authorize = (role: Role): string => {
  switch (role) {
    case 'admin':
      return 'You can do anything';
    case 'user':
      return 'You can do something';
    case 'guest':
      return 'You can do nothing';
    default:
      // never reach here util we add a new role
      return unknownRole(role);
  }
};

console.log(authorize('admin'));


Functions

function name(parameter: type, parameter:type,...): returnType {
   // do something
}

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

function echo(message: string): void {
    console.log(message.toUpperCase());
} // not returning any value return void

function add(a: number, b: number) {
    return a + b;
} // Infers return type by self


If a function has different branches that return different types, the TypeScript compiler may infer the union type or any type.

Function types

Function types ensure that the parameters and return values are of the expected types.

Prevents runtime errors caused by passing wrong arguments.

Good for type safety.

// Function type: takes two numbers, returns a number
let add: (a: number, b: number) => number;

add = (x, y) => x + y; // ✅ Works
add = (x, y) => `${x}${y}`; // ❌ Error: must return number

Reusable singnatures

type MathOperation = (a: number, b: number) => number;

const multiply: MathOperation = (a, b) => a * b;
const divide: MathOperation = (a, b) => a / b;

By using the type inference, you can significantly reduce the amount of code with annotations.

Supports Higher-Order Functions

Function types make it easier to pass functions as arguments or return them from other functions.

function operate(fn: (a: number, b: number) => number, x: number, y: number) {
    return fn(x, y);
}

console.log(operate((a, b) => a + b, 5, 3)); // 8

let add: (x: number, y: number) => number; // Function declaration

add = function (x: number, y: number) { // Function definition
    return x + y;
};

// If you assign other functions whose type doesn’t match
// the add variable, TypeScript will issue an error
  • let add: (x: number, y: number) => number;

    • This declares a variable add whose type is a function that:
      • Takes two parameters (x and y) of type number
      • Returns a number
    • At this point, add is declared but not assigned.
  • add = function (x: number, y: number) { ... }

    • Here you assign an anonymous function to add that matches the declared type.
let add: (a: number, b: number) => number =
    function (x: number, y: number) {
        return x + y;
    };
  • This is a single-step declaration and assignment.
  • let add: (a: number, b: number) => number = ...
    • Declares add with the same function type as before.
    • Immediately assigns a function to it.
  • Parameter names in the type annotation (a, b) do not have to match the parameter names in the actual function (x, y).
    Only the types and order must match.

Key Points

  1. Function type syntax:
    (paramName: Type, paramName: Type) => ReturnType
  2. Parameter names in type vs. implementation:
    Names can differ; only types and positions matter.
  3. Two-step vs. one-step:
    • Two-step: Declare type first, assign later. ( Declare then assign )
    • One-step: Declare type and assign immediately. ( Declare and assign )
  4. Type safety:
    TypeScript ensures that the assigned function matches the declared type.
Optional parameters

you can call a function without passing any arguments even though the function specifies parameters. Therefore, JavaScript supports the optional parameters by default.

The optional parameters must appear after the required parameters in the parameter list. Means at the end of required parameters.

function multiply(a: number, b: number, c?: number): number {

    if (typeof c !== 'undefined') {
        return a * b * c;
    }
    return a * b;
}
// check if the argument is passed to the function by using the
// expression typeof c !== 'undefined'.

// Note that if you use the expression if(c) to check if an argument
// is not initialized, you would find that the empty string or zero
// would be treated as undefined.

function multiply(a: number, b?: number, c: number): number {
// b gives error
    if (typeof c !== 'undefined') {
        return a * b * c;
    }
    return a * b;
}

Default parameters

We can pass default parameters in functions.

if you don’t pass arguments or pass the undefined into the function when calling it, the function will take the default initialized values for the omitted parameters. As below:

function applyDiscount(price, discount = 0.05) {
    return price * (1 - discount);
}

console.log(applyDiscount(100)); // 95

Notice that you cannot include default parameters in function type definitions. The following code will result in an error:

let promotion: (price: number, discount: number = 0.05) => number; // ERROR

default parameters are optional means we can omit/ avoid the passing of the parameters in the function.

Optional parameters must come after the required parameters. However, default parameters don’t need to appear after the required parameters.

When a default parameter appears before a required parameter, you need to explicitly pass undefined to get the default initialized value.

Rest Parameters

A rest parameter allows a function to accept zero or more arguments of the specified type. In TypeScript, the rest parameters follow these rules:

  • A function has only one rest parameter.
  • The rest parameter appear last in the parameter list.
  • The type of the rest parameter is an array type.
  • represent an indefinite number of arguments as an array.
  • Use rest parameters to allow a function to accept a variable number of arguments with the same or different types.
  • Use ...args type[] syntax to define rest parameters with the same type.
  • Use ...args (type1 | type2 ) [] syntax to define rest parameters with different types.

Rest parameters with a single type


function getTotal(...numbers: number[]): number {
    let total = 0;
    numbers.forEach((num) => total += num);
    return total;
}

Rest parameters with a multiple type


function combine(...args: (number | string)[]): [number, string] {
  let total = 0;
  let str = '';
  args.forEach((arg) => {
    if (typeof arg === 'number') {
      total += arg;
    } else if (typeof arg === 'string') {
      str += arg;
    }
  });

  return [total, str];
}

const [total, str] = combine(3, 'Happy', 2, 1, ' New Year');

console.log({ total });
console.log({ str });

Function Overloading

Function overloading allows you to define multiple signatures for a single function and provide one implementation that handles all defined signatures.

Function overloading enables a function to handle different types of arguments.

TypeScript compiler uses the function signatures to perform compile-time type checking to ensure type safety.

TypeScript function overloading is unlike the function overloading supported by other statically typed languages such as Java.

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

function add(a: any, b: any): any {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a === 'string' && typeof b === 'string') {
        return a + b;
    }
    throw new Error('Invalid arguments');
}

console.log(add(10, 20));  // 30
console.log(add('Hello, ', 'world!'));  // 'Hello, world!

Function overloading with optional parameters

the number of required parameters must be the same. If an overload has more parameters than the other, you need to make the additional parameters optional
If an overload has more parameters than the other, you need to make the additional parameters optional. For example:

function sum(a: number, b: number): number;
function sum(a: number, b: number, c: number): number;

function sum(a: number, b: number, c?: number): number {
    if (c) return a + b + c;
    return a + b;
}

The third parameter is optional. If you don’t make it optional, you will get an error.

When a function is a property of a class, it is called a method. TypeScript also supports method overloading.

Classes

In JS

class Person {
    ssn;
    firstName;
    lastName;

    constructor(ssn, firstName, lastName) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

In TS

class Person {
    ssn: string;
    firstName: string;
    lastName: string;

    constructor(ssn: string, firstName: string, lastName: string) {
        this.ssn = ssn;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName(): string {
        return `${this.firstName} ${this.lastName}`;
    }
}

you cannot initialize the ssn with a number. The following code will result in an error:

let person = new Person(171280926, 'John', 'Doe'); // Error

TypeScript Access Modifiers


The private modifier limits the visibility to the same class only.
You can access that property or method within the same class.
Any attempt to access private properties or methods outside the class will result in an error at compiled time.

The public modifier allows class properties and methods to be accessible from all locations. If you don’t specify any access modifier for properties and methods, they will take the public modifier by default.

The protected modifier allows properties and methods of a class to be accessible within the same class and subclasses.

When a class (child class) inherits from another class (parent class), it is a subclass of the parent class.

The TypeScript compiler will issue an error if you attempt to access the protected properties or methods from anywhere else.

To make the code shorter, TypeScript allows you to both declare properties and initialize them in the constructor like this:

class Person {
  constructor(
    protected ssn: string,
    private firstName: string,
    private lastName: string
  ) {}

  getFullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
}

Read Only

  • Use the readonly access modifier to mark a class property as immutable.
  • A readonly property must be initialized as a part of the declaration or in the constructor of the same class.

readonly vs. const

The following shows the differences between readonly and const:

readonlyconst
Use forClass propertiesVariables
InitializationIn the declaration or in the constructor of the same classIn the declaration
class Person {
    readonly birthDate: Date;

    constructor(birthDate: Date) {
        this.birthDate = birthDate;
    }
}

let person = new Person(new Date(1990, 12, 25));
person.birthDate = new Date(1991, 12, 25); // Compile error

// can do like this
class Person {
    constructor(readonly birthDate: Date) {
    }
}

Getter and Setter
  • Use TypeScript getters/setters to control the access properties of a class.
  • The getter/setters are also known as accessors/mutators.
  • To avoid repeating the check, you can use setters and getters.
class Person {
  private _age: number;
  private _firstName: string;
  private _lastName: string;

  constructor(age: number, firstName: string, lastName: string) {
    this._age = age;
    this._firstName = firstName;
    this._lastName = lastName;
  }

  public get age() {
    return this._age;
  }

  public set age(theAge: number) {
    if (theAge <= 0 || theAge >= 200) {
      throw new Error('The age is invalid');
    }
    this._age = theAge;
  }

  public getFullName(): string {
    return `${this._firstName} ${this._lastName}`;
  }
}

let person = new Person(22, 'John', 'Doe');
person.age = 29;

console.log(person.age);

Inheritance

A class can reuse the properties and methods of another class. This is called inheritance in TypeScript.
JavaScript uses prototypal inheritance, not classical inheritance like Java or C#. ES6 introduces the class syntax that is simply the syntactic sugar of the prototypal inheritance. TypeScript supports inheritance like ES6.   

Constructor 

Because the Person class has a constructor that initializes the firstName and lastName properties, you need to initialize these properties in the constructor of the Employee class by calling its parent class’ constructor.

To call the constructor of the parent class in the constructor of the child class, you use the super() syntax. For example:   

class Person {
  constructor(private firstName: string, private lastName: string) {}
  getFullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
  describe(): string {
    return `This is ${this.firstName} ${this.lastName}.`;
  }
}

class Employee extends Person {
    constructor(
        firstName: string,
        lastName: string,
        private jobTitle: string) {
       
        // call the constructor of the Person class:
        super(firstName, lastName);
    }

    describe(): string {
        return super.describe() + `I'm a ${this.jobTitle}.`;
    }
}

let employee = new Employee('John', 'Doe', 'Web Developer');

console.log(employee.getFullName()); console.log(employee.describe());

Above super.describe() is the method overriding which uses the methods of parent class.

Static method and properties

Static property

Unlike an instance property, a static property is shared among all instances of a class.
To declare a static property, you use the static keyword. To access a static property, you use the className.propertyName syntax. For example:

class Employee {
    static headcount: number = 0;

    constructor(
        private firstName: string,
        private lastName: string,
        private jobTitle: string) {

        Employee.headcount++;
    }
}

Static methods

class Employee {
    private static headcount: number = 0;

    constructor(
        private firstName: string,
        private lastName: string,
        private jobTitle: string) {

        Employee.headcount++;
    }

    public static getHeadcount() {
        return Employee.headcount;
    }
}


In practice, you will find a library that contains many static properties and methods like the Math object. It has PI, E, … static properties and abs(), round(), etc., static methods.

Abstract Classes

An abstract class is typically used to define common behaviors for derived classes to extend.

Unlike a regular
class, an abstract class cannot be instantiated directly.
Typically, an abstract class contains one or more abstract methods.

An abstract method does not contain implementation. It only defines the signature of the method without including the method body. An abstract method must be implemented in the derived class.

To use an abstract class, you need to inherit it and provide the implementation for the abstract methods.

abstract class Employee {
  constructor(private firstName: string, private lastName: string) {}
  abstract getSalary(): number;
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }
  compensationStatement(): string {
    return `${this.fullName} makes ${this.getSalary()} a month.`;
  }
}

class FullTimeEmployee extends Employee {
    constructor(firstName: string, lastName: string, private salary: number) {
        super(firstName, lastName);
    }
    getSalary(): number {
        return this.salary;
    }
}

class Contractor extends Employee {
  constructor(
    firstName: string,
    lastName: string,
    private rate: number,
    private hours: number
  ) {
    super(firstName, lastName);
  }
  getSalary(): number {
    return this.rate * this.hours;
  }
}

let john = new FullTimeEmployee('John', 'Doe', 12000);
let jane = new Contractor('Jane', 'Doe', 100, 160);

console.log(john.compensationStatement());
console.log(jane.compensationStatement());

let employee = new Employee('John','Doe');// Error

Interfaces
  • TypeScript interfaces define contracts in your code and also provide explicit names for type-checking.
  • Interfaces may have optional properties or read-only properties.
  • Interfaces can be used as function types.
  • Interfaces are typically used as class types that make a contract between unrelated classes.
// Normal way of working
function getFullName(person: {
    firstName: string;
    lastName: string
}) {
    return `${person.firstName} ${person.lastName}`;
}

let person = {
    firstName: 'John',
    lastName: 'Doe'
};

console.log(getFullName(person));

Using interface

interface Person {
    firstName: string;
    lastName: string;
}

function getFullName(person: Person) {
    return `${person.firstName} ${person.lastName}`;
}

let john = {
    firstName: 'John',
    lastName: 'Doe'
};

console.log(getFullName(john));

After defining the Person interface, you can use it as a type.

They are purely for type-checking and are removed during compilation, ensuring code clarity and consistency without runtime overhead.

With object destructuring

function getFullName({ firstName, lastName }: Person) {
  return `${firstName} ${lastName}`;
}

The getFullName() function will accept any object that has at least two string properties with the name firstName and lastName.

Below works good:

let jane = {
  firstName: 'Jane',
  middleName: 'K.',
  lastName: 'Doe',
  age: 22,
};

Optional properties


interface Person {
    firstName: string;
    middleName?: string;
    lastName: string;
}

function getFullName(person: Person) {
    if (person.middleName) {
        return `${person.firstName} ${person.middleName} ${person.lastName}`;
    }
    return `${person.firstName} ${person.lastName}`;
}

ReadOnly Properties

interface Person {
  readonly ssn: string;
  firstName: string;
  lastName: string;
}

let person: Person;
person = {
  ssn: '171-28-0926',
  firstName: 'John',
  lastName: 'Doe',
};

person.ssn = '171-28-0000'; // Error

Function types

To describe a function type, you assign the interface to the function signature that contains the parameter list with types and returned types. For example:

interface StringFormat {
    (str: string, isUpper: boolean): string
}

let format: StringFormat;

format = function (str: string, isUpper: boolean) {
    return isUpper ? str.toLocaleUpperCase() : str.toLocaleLowerCase();
};

console.log(format('hi', true));

The StringFormat interface ensures that all the callers of the function that implements it pass in the required arguments: a string and a boolean.

The following code also works perfectly fine even though the lowerCase is assigned to a function that doesn’t have the second argument:

let lowerCase: StringFormat;
lowerCase = function (str: string) {
    return str.toLowerCase();
}

console.log(lowerCase('Hi', false));
// Called with 2 arguments

Class Types

Implementing Interfaces in Classes.
Classes must implement all properties and methods defined in the interface.

interface Json {
  toJson(): string;
}

class Person implements Json {
  constructor(private firstName: string, private lastName: string) {}
  toJson(): string {
    return JSON.stringify(this);
  }
}

let person = new Person('John', 'Doe');
console.log(person.toJson());

Extending Interfaces

  • An interface can extend one or multiple existing interfaces.
  • An interface also can extend a class. If the class contains private or protected members, the interface can only be implemented by the class or subclasses of that class.
interface Shape { color: string; }
interface Square extends Shape { sideLength: number; }
const square: Square = { color: "blue", sideLength: 10 };

Interface extends other interfaces.
Interface extends other classes.

Interfaces vs Abstract Classes


AspectInterfacesAbstract Classes
PurposeDefine contractual structure.Provide common functionality and structure.
ImplementationContains only method signatures.Can contain implemented methods and abstract methods.
Multiple InheritanceSupports multiple interface implementation.Supports single class inheritance.
Implementation FlexibilityNo implementation code in interfaces.Mixes implemented and abstract methods.
ExtensibilityEasily extendable by adding new properties/methods.Can provide shared methods for derived classes.
ConstructorsNo constructors in interfaces.Can have constructors for initialization.
Type CheckingEnsures objects adhere to the structure.Provides a common type and functionality.
InstantiationInterfaces can’t be instantiated.Abstract classes can’t be instantiated directly.
UsageDesigning contracts and structure.Sharing functionality among related classes.

Intersection Types


To create 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 as follows:

When you intersect types, the order of the types doesn’t matter. 

type typeAB = typeA & typeB;

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

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

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

type Employee = Identity & Contact;

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

type Customer = BusinessPartner & Contact;

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

type Employee = Identity & BusinessPartner & Contact;

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

// Notice both BusinessPartner and Identity have the property name with the same type.
// If they do not, then you will have an error.


Type Guards


Type Guards allow you to narrow down the type of a variable within a conditional block.

typeOf

if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
}

instanceOf

type BusinessPartner = Customer | Supplier;

if (partner instanceof Customer) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer'
        : 'Credit issue';
}

in

The in operator carries a safe check for the existence of a property on an object. You can also use it as a type guard. 

function signContract(partner: BusinessPartner) : string {
    let message: string;
    if ('isCreditAllowed' in partner) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer' : 'Credit issue';
    } else {
        // must be Supplier
        message = partner.isInShortList() ? 'Sign a new contract the supplier ' : 'Need to evaluate further';
    }
    return message;
}

User-defined Type Guards

User-defined type guards allow you to define a type guard or help TypeScript infer a type when you use a function.

A user-defined type guard function is a function that simply returns arg is aType

function isCustomer(partner: any): partner is Customer {
    return partner instanceof Customer;
}

function signContract(partner: BusinessPartner): string {
    let message: string;
    if (isCustomer(partner)) {
        message = partner.isCreditAllowed() ? 'Sign a new contract with the customer'
        : 'Credit issue';
    } else {
        message = partner.isInShortList() ? 'Sign a new contract with the supplier'
        : 'Need to evaluate further';
    }

    return message;
}

Type Assertions


  • Type assertion allows you to assign a new type to a value.
  • Use the as keyword or <> operator to perform a type assertion.
let enteredText = (el as HTMLInputElement).value;

// OR

let input = <HTMLInputElement>document.querySelector('input[type="text"]');
console.log(input.value);

Generics

TypeScript generics allow you to write reusable and generalized forms of functions, classes, and interfaces

function getRandomElement<T>(items: T[]): T {
    let randomIndex = Math.floor(Math.random() * items.length);
    return items[randomIndex];
}

it can work with any data type including string, number, object,…

you can freely use other letters such as A, B C, …

The compiler looks at the argument and sets T to its type.

Generic functions with multiple types


function merge<U, V>(obj1: U, obj2: V) {
    return {
        ...obj1,
        ...obj2
    };
}

let result = merge(
    { name: 'John' },
    { jobTitle: 'Frontend Developer' }
);

console.log(result);

The following are the benefits of TypeScript generics:

  • Leverage type checks at the compile time.
  • Eliminate type castings.
  • Allow you to implement generic algorithms.

TypeScript Generic Constraints


Above works fine and below also.

let person = merge(
    { name: 'John' },
    25
);

console.log(person); // { name: 'John' }
// WITH NO ERRORS

But to make the generic function to take only the required details throws error if any data passed is changed. As below

function merge<U extends object, V extends object>(obj1: U, obj2: V) {
    return {
        ...obj1,
        ...obj2
    };
} // It works with object type only.

Using type parameters in generic constraints

TypeScript allows you to declare a type parameter constrained by another type parameter.

function prop<T, K>(obj: T, key: K) {
    return obj[key];
} // compiler error
// Type 'K' cannot be used to index type 'T'.

// Error fixing
function prop<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

// Works
let str = prop({ name: 'John' }, 'name'); console.log(str);


However, if you pass a key that doesn’t exist on the first argument, the compiler will issue an error:

let str = prop({ name: 'John' }, 'age');
// Error
// Argument of type '"age"' is not assignable to parameter of type '"name"'.


TypeScript Generic Classes


class className<T>{
    //...
}

class className<K,T>{
    //...
    // Multiple generic types
}

The generic constraints are also applied to the generic types in the class:

class className<T extends TypeA>{
    //...
}

Placing the type parameter on the class allows you to develop methods and properties that work with the same type.

TypeScript Generic Interfaces


A generic interface allows you to create an interface that can work with different types while maintaining type safety.

A generic interface has a generic type parameter list in angle brackets <> following the name of the interface

interface interfaceName<T> {
    // ...
}

interface interfaceName<U,V> {
    // ...
}

The type parameter list can have one or multiple types. 

1) Generic interfaces that describe object properties


interface Pair<K, V> {
    key: K;
    value: V;
}

let month: Pair<string, number> = {
    key: 'Jan',
    value: 1
};

console.log(month);

2) Generic interfaces that describe methods


interface Collection<T> {
    add(o: T): void;
    remove(o: T): void;
}

class List<T> implements Collection<T>{
    private items: T[] = [];

    add(o: T): void {
        this.items.push(o);
    }
    remove(o: T): void {
        let index = this.items.indexOf(o);
        if (index > -1) {
            this.items.splice(index, 1);
        }
    }
}

let list = new List<number>();

for (let i = 0; i < 10; i++) {
    list.add(i);
}


3) Generic interfaces that describe index types


Generic interfaces with index types allow you to define objects whose property names are dynamic while keeping the values strongly typed. This is useful when you don't know the property names in advance but do know the type of the values.


interface StringDictionary {
  [key: string]: string;
}

const person: StringDictionary = {
  firstName: "Sunil",
  lastName: "Kumar",
  city: "Delhi"
};

console.log(person.firstName);
console.log(person["city"]);


Generic Interface with Both Properties and Index

interface ProductMap<T> {
    category: string;
    [productName: string]: T | string;
}

const products: ProductMap<number> = {
    category: "Electronics",
    Laptop: 65000,
    Mobile: 35000,
    Tablet: 25000
};






TypeScript

It is a superset of JavaScript. TS builds on top of Javascript. The main goals of TypeScript are: Introduce optional types to JavaScript....