Getting started

What is typescript ?

#### Created from microsoft to address some of the shortcomings of JavaScript, TypeScript is there to make your JS robust

TypeScript is a superset of JavaScript that adds static typing to the language. Unlike JavaScript, where variables can hold any type of value and types are inferred at runtime, TypeScript allows you to specify types explicitly during development.

These types are checked by the TypeScript compiler before your code is transpiled into regular JavaScript, helping catch type-related errors early in the development process.

#### Ts Simply makes JS be sort of a Static type language compared to Dynamic type that JS is.

Why do we need TypeScript

  1. Static Typing and Type Safety: TypeScript’s most significant advantage is static typing. By declaring types for variables, function parameters, and return values, you can catch type-related bugs before they reach the runtime environment. This enhances code quality, reduces runtime errors, and improves maintainability.
  2. Enhanced Tooling: TypeScript-aware IDEs and code editors provide improved code suggestions, auto-completion, and better code navigation due to the availability of type information. This makes development more efficient and helps developers understand APIs and code structures more easily.
  3. Readability and Maintainability: Explicitly declared types make the code more self-documenting, making it easier for other developers (or even your future self) to understand the intended data structures and flows. This is especially beneficial in larger codebases.
  4. Refactoring and Codebase Evolution: TypeScript aids in refactoring code by allowing you to safely change types without manually searching and updating each occurrence. This becomes crucial as projects evolve over time.
  5. Interface and Contract Enforcement: TypeScript’s type system allows you to define interfaces and contracts for APIs, ensuring that functions and objects adhere to specific structures. This helps prevent potential bugs when integrating various parts of an application.
  6. Early Error Detection: Type errors are caught at compile time, which means you can catch mistakes before running your code. This can save a lot of time during development and testing phases.
  7. Migration from JavaScript: If you’re already using JavaScript, adopting TypeScript can be done gradually. TypeScript is a superset of JavaScript, meaning that valid JavaScript code is also valid TypeScript code. You can start adding types to your existing JavaScript codebase and gradually transition to using more advanced TypeScript features.
  8. Popular Frameworks and Libraries: TypeScript is widely used in conjunction with popular frontend frameworks like Angular, React, and Vue.js. These frameworks provide TypeScript typings, enhancing the development experience and ensuring that you’re using their APIs correctly.
  9. Large-Scale Applications: TypeScript is particularly useful for large-scale applications where code organization, maintainability, and collaboration among developers become more challenging. The type system provides more control over data flow and interactions.

Differences Between TypeScript and Vanilla JavaScript:

  • [/] Type System:
    • ➡️ TypeScript enforces static types, meaning you declare the type of variables, function parameters, and return values. In vanilla JavaScript, types are determined at runtime.
  • [/] Type Annotations:
    • ➡️ TypeScript requires type annotations (e.g., number, string, boolean) to indicate the type of variables, parameters, and functions. Vanilla JavaScript doesn’t have this requirement.
  • [/] Compile Step:
    • ➡️ TypeScript code needs to be transpiled into JavaScript using the TypeScript compiler. Vanilla JavaScript is executed directly by the browser or JavaScript engine.
  • [/] Tooling and IDE Support:
    • ➡️ TypeScript-aware tools and IDEs provide richer features, such as intelligent code completion and type-related suggestions, which are not as robust in vanilla JavaScript.
  • [/] Early Error Detection:
    • ➡️ TypeScript detects errors at compile time, while vanilla JavaScript may only reveal errors during runtime, potentially leading to more debugging time.
  • [/] Code Complexity Management:
    • ➡️ TypeScript’s type system aids in managing the complexity of large-scale applications, reducing potential issues arising from data interactions.
  • [/] Compatibility with ES6 and Beyond:
    • ➡️ TypeScript supports features from modern JavaScript versions (ES6 and beyond), making it possible to use the latest language features even when targeting older browsers.

Drawbacks of TS

While TypeScript offers numerous benefits, there are also some drawbacks and challenges associated with using the language. It's important to be aware of these aspects when considering whether TypeScript is the right choice for a particular project:

  1. Learning Curve: Transitioning from JavaScript to TypeScript requires learning new concepts related to type annotations, interfaces, and other TypeScript-specific features. This initial learning curve can slow down development at the beginning.
  2. Verbose Syntax: The requirement to explicitly declare types and annotate variables and functions can lead to more verbose code compared to dynamically typed languages like JavaScript.
  3. Development Speed: While TypeScript’s type checking helps catch errors early, it can also slow down the development process due to the time spent on type annotations and potential issues arising from stricter type enforcement.
  4. Build Process: TypeScript code needs to be transpiled to JavaScript before it can run in a browser or on a Node.js server. This extra build step can add complexity to the development workflow.
  5. Compatibility with Libraries: While many popular libraries and frameworks have TypeScript typings available, not all third-party libraries and plugins may have well-maintained type definitions. This can lead to issues when integrating external code.
  6. Type Complexity: In complex applications, managing complex type hierarchies and interactions can become challenging. Balancing the need for precise types with code simplicity can be a delicate task.
  7. Lack of Full Type Safety: While TypeScript enhances type safety compared to JavaScript, it doesn’t eliminate all potential runtime errors. Developers still need to ensure their code logic is correct.
  8. Development Team Familiarity: If your development team is more experienced with JavaScript or comes from a dynamically typed background, adopting TypeScript may require additional training and adaptation.
  9. Tooling and IDE Support: While TypeScript-aware tools and IDEs provide valuable features, not all development environments may offer robust TypeScript support.
  10. Increased Maintenance Overhead: Maintaining type definitions, especially in larger codebases, requires additional effort. As code evolves, type annotations may need to be updated to reflect changes accurately.
  11. Performance Overhead: The TypeScript compiler adds a layer of abstraction that might impact runtime performance, although this impact is generally minimal and can be outweighed by the benefits of early error detection.

Setting up TS

Setting up a development environment for TypeScript involves a few steps, including installing necessary tools and configuring your project to work with TypeScript.

Tools and installation

Install Node.js and npm:

TypeScript relies on Node.js and its package manager, npm, for various tasks. Install Node.js by downloading the installer from the official website. Once Node.js is installed, npm will be available as well.

Create a Project Directory:

Create a new directory for your TypeScript project and navigate to it using your command line interface.

Installing Typescript

Install the typescript compiler globally

npm i -g typescript
// Install typescript gloabbaly so that we can access it from everywhere

TO verify our installation, we simply type

tsc -v
#We will get the installed version

First TS Program.

Our first Program

In your directory create an index.ts

Since TS is a superset of JavaScript, means all valid JS file is a valid TS File. (index.ts)

console.log("Hello world")

Since no browser understand TS, we are going to need to compile the TS code to valid JS code

tsc index.ts

This will create a new index.js file that will have the transpiled js code for our environment to understand.

TypeScript with ES5

Example of something we definitely don't want in our JS

let age: number = 34
age = 45

Above everything seem to work well, but let's take a look at what we got in js

var age = 34
age = 435

We Certainly don’t want to use a var for this transpilation, what the heck is going on ?

By default TS uses ES5 and that is why we are seeing the var in our code, we certainly wan to change that and that is when we need the configurations.

Configuring the TypeScript Compiler

Configuring the TypeScript compiler (tsc) involves creating a tsconfig.json file in your project directory. This file specifies compiler options, project settings, and other TypeScript-related configurations.

Creating our configuration file

In our project directory, create a file named tsconfig.json. This file will hold your TypeScript compiler configuration.

Or simply run the init command to create the TypeScript Starter config file

tsc --init

The Above command will generate a tsc file with a whole lot of commands but we most of the time are interested in a handful of them. in our case here is a few that are kinds interesting

{
  "target": "ES2016" /*Set the varsion of js we wan to compile to*/,
  "module": "commonjs" /* Specify what module code is generated. */,
  "rootDir": "./src" /* Specify the root directory where our typescript is in */,
  "outDir": "./dist" /* Specify an output folder for all emitted files. */,
  "removeComments": true /* No comment will be sent in our js file, to make our code short */,
  "noEmitOnError": true /* When there is an eror in TS, then don't compile ! */,
  "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
  "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
  "strict": true /* Enable all strict type-checking options. */
}

Compiling our TypeScript after configuration

Now that we have added our configuration, we simply can run the tsc command and our compilation will be done for us

Compiling our files after configuration

tsc

Debugging TypeScript Applications

To debug our typescript code we can use VSCODE to debug our typescript file.

Fundamentals

Build In types

TypeScript provides a variety of built-in types that you can use to define the shape and behavior of your data. These types help you ensure type safety, catch errors early, and write more robust code. Here are some of the most commonly used built-in types in TypeScript:

Types Scrip types

## JS has a variety of types such as

Declaring our primitive values

Typescript variables

let sales: number = 123_456_789
let course: string = "Typescript"
let isPublished: boolean = true
let level

Automatic annotation.

You can also declare variables without type annotation, typescript will do it for you

let sales = 123_456_789 //  number= 123456789
let course = "Typescript" //  string= 'Typescript'
let isPublished = true //  boolean= true
let level //  undefined = undefined

Since the last one has no type, typescript will assign it the any type

Javascirpt Output

"use strict"
let sales = 123456789
let course = "Typescript"
let isPublished = true
let level

TypeScript variables

TypeScript extends this list by adding new types such as

The any type

#any The any type in TypeScript is a special type that represents a value of any type. When you declare a variable with the any type, you’re essentially telling TypeScript to disable type checking for that variable. This means that you can assign any value to a variable of type any, and TypeScript won’t raise any type-related errors or warnings.

Here's how the any type works in TypeScript:

let value: any = "Hello"
value = 42 // No type error
value = true // No type error
 
function dynamicReturnType(): any {
  return "This function returns any type."
}
const result: any = dynamicReturnType()
Drawbacks of any

While the any type provides flexibility, it comes with drawbacks:

  1. [i] Type Safety:
    • ➡️ By using any, you’re essentially opting out of TypeScript’s type system, which defeats the purpose of using TypeScript for type checking and safety.
  2. [i] Loss of Type Information:
    • ➡️ When you use any, you lose the benefits of TypeScript’s type inference and type checking, making your code more error-prone.
  3. [i] Code Maintenance:
    • ➡️ Code written with a lot of any types can become difficult to understand and maintain, especially as your codebase grows.

It's generally recommended to avoid using the any type as much as possible and instead leverage TypeScript's static typing to ensure code correctness. If you find yourself using any frequently, consider exploring more precise types, using type annotations, and making use of type unions and intersection types to maintain better type safety in your TypeScript code.

If you don't want the type annotation error to be turned off you can set the config of the noImplicitAny to true, but only do this if you really know what you are doing.

The array type

In TypeScript, the array type is used to define and work with arrays of values. Arrays are collections of items of the same type or a union of different types. TypeScript provides various ways to define and manipulate array types.

Defining your array types

Defining Array Types:

You can define an array type by using the Type[] syntax, where Type represents the type of elements in the array.

let numbers: number[] = [1, 2, 3, 4];
let names: string[] = ["Alice", "Bob", "Charlie"];

Alternatively, you can use the Array<Type> syntax: also known as Array generic type:

let numbers: Array<number> = [1, 2, 3, 4];
let names: Array<string> = ["Alice", "Bob", "Charlie"];

Array Methods and Properties:

TypeScript provides type-aware support for array methods and properties.

let numbers: number[] = [1, 2, 3, 4];
 
numbers.push(5);    // Works
numbers.pop();      // Works
numbers.length;     // Returns the length
 

Array Union types

You can use union types to represent arrays that can contain multiple types of values:

let mixedArray: (number | string)[] = [1, "two", 3, "four"];

Read-Only Arrays:

You can create read-only arrays using ReadonlyArray<Type>:

let readOnlyNumbers: ReadonlyArray<number> = [1, 2, 3, 4];

This prevents you from modifying the array's contents after creation.

You can also define a readOnly array with mixed types

const person: ReadonlyArray<string | number> = ["Hello", 42, "World", 13];

Array Type Inference:

TypeScript can infer array types when initialized with values:

let inferredNumbers = [1, 2, 3] // inferredNumbers: number[]

TypeScript automatically infers the array type based on the provided values.

Array of arrays

Recursive types, are self-referential, and are often used to describe infinitely nestable types. For example, consider infinitely nestable arrays of numbers

;[3, 4, [5, 6, [7], 59], 221]

You may read or see things that indicate you must use a combination of interface and type for recursive types. As of TypeScript 3.7 this is now much easier, and works with either type aliases or interfaces.

type NestedNumbers = number | NestedNumbers[]
 
const val: NestedNumbers = [3, 4, [5, 6, [7], 59], 221]
 
if (typeof val !== "number") {
  val.push(41)
  //(method) Array<NestedNumbers>.push(...items: NestedNumbers[]): number
  //val.push("this will not work")
  //Argument of type 'string' is not assignable to parameter of type 'NestedNumbers'.
}

The Tuple type

Tuples are another feature in TypeScript that allow you to represent an array with a fixed number of elements, where each element can have a different type. Tuples provide a way to express a specific structure for your data, ensuring that the correct types are used at specific positions within the array.

We often use them when working with a pair of value

Tuple types allow you to define arrays with fixed lengths and specific types for each element:

To define a tuple, use square brackets and specify the types for each element at their respective positions:

let person: [string, number] = ["Alice", 30];

Tuple types enforce the order and number of elements in the array.

Accessing Tuple Elements

You can access tuple elements using zero-based indexing:

let name: string = person[0];
let age: number = person[1];

As a best practice, restrict your tuple to two values, other than that, you might have trouble working with you data.

Tuple Type Inference:

TypeScript can infer tuple types when initialized with values:

let data = ["Hello", 42] // data: [string, number]

TypeScript automatically infers the tuple type based on the provided values.

Optional Elements

Tuple elements can be optional by using the ? modifier:

let optionalTuple: [string, number?] = ["Alice"]

In this case, the second element (number) is optional.

Rest Elements:

You can use the ... syntax to represent the remaining elements as an array:

let coordinates: [number, number, ...number[]] = [1, 2, 3, 4, 5]
Mixing Types:

Tuples allow mixing different types, but you need to ensure that the order and types of elements match:

let mixed: [string, number, boolean] = ["Hello", 42, true];
Limitations:

Tuples provide type safety for fixed-size arrays but are not intended for large datasets or collections. In scenarios where the structure of your data may change or the number of elements is dynamic, other data structures like arrays or objects may be more appropriate.

Benefits:

Tuples are useful when you want to ensure a specific structure and a fixed number of elements at specific positions. They provide better type safety than plain arrays when dealing with mixed data types.

Tuples are often used to represent pairs or small sets of related data, such as coordinates, point values, or function results with multiple types of data

The Enum Type

Enums, short for “enumerations,” are a powerful feature in TypeScript that allow you to define a set of named values. Enums provide a way to create more expressive and self-documenting code by giving meaningful names to specific values

Enum represents a list of related constants

Creating Enums:

You can create an enum using the enum keyword followed by a list of names (identifiers) representing the possible values.

enum Color {
  Red,
  Green,
  Blue,
}

By default, enum members are assigned numeric values starting from 0,1...

In this example, Color is an enum with three members: Red, Green, and Blue.

Custom Enum Values:

You can assign custom values to enum members:

enum Status {
  Active = 1,
  Inactive = 2,
  Pending = 3,
}

In this case, Active will have a value of 1, Inactive a value of 2, and so on. If no value is provided, the first member has a value of 0, and subsequent members are assigned values incrementing by 1.

Accessing Enum Members:

You can access enum members using dot notation:

let currentStatus: Status = Status.Active;
console.log(currentStatus); // Output: 1
Reverse Mapping:

Enums in TypeScript have a reverse mapping feature. You can get the name of an enum member from its value:

enum Status {
  Active = 1,
  Inactive = 2,
  Pending = 3,
}
 
let statusName: string = Status[2]
console.log(statusName) // Output: "Inactive"
Enum as Types:

Enums can also be used as types to restrict variable values:

let userStatus: Status = Status.Active

Using enums as types helps prevent accidental assignments of invalid values.

Iterating over Enums:

Since enums are backed by numeric values, you can iterate over them using loops:

for (let statusKey in Status) {
  if (isNaN(Number(statusKey))) {
    console.log(statusKey)
    // Output: "Active", "Inactive", "Pending"
  }
}
if (isNaN(Number(statusKey))) {
  console.log(statusKey) // Output: "Active", "Inactive", "Pending"
}
String Enums:

String enums allow you to use strings as enum values instead of numeric values:

 
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}
Const Enums:

Using the const keyword with an enum declaration ensures that the enum will not be transpiled into JavaScript, but its values will be inlined where used. This can lead to optimization in certain scenarios.

Use cases of enum

Enums are useful when you have a finite set of related values that represent different options or states. They enhance code readability and maintainability by providing meaningful names to values that might otherwise be represented by numbers or strings

Enums are a great way to model certain types of data, but they might not be the best fit for all scenarios. Be cautious when using enums with a large number of members, as it can lead to bloated code. In summary, enums in TypeScript provide a way to create a set of named values, making your code more expressive and self-documenting. They are especially useful for representing options, states, or categories in your code.

Functions

In TypeScript, you can define the types of parameters and return values for functions. This enhances code clarity and provides type checking:

As a best practice we should always properly annotate our functions, so that all the parameters and return types are properly annotated, especially if you are building an API for other people to use.

Basics Usage

function add(x: number, y: number): number {
    return x + y;
}

Here, the function add takes two parameters of type number and returns a value of type number.

Optional Parameters:

You can make function parameters optional using the ? modifier:

function greet(name: string, age?: number): string {
  if (age) {
    return `Hello, ${name}! You are ${age} years old.`
  } else {
    return `Hello, ${name}!`
  }
}

The age parameter is optional, and you can call the function with or without providing it.

Default Parameters:

You can also set default values for parameters:

function greet(name: string, message: string = "Hello"): string {
    return `${message}, ${name}!`;
}

Function Types:

Functions can be used as types:

type MathOperation = (x: number, y: number) => number;
let add: MathOperation = (x, y) => x + y;

This example defines a type MathOperation and assigns the add function to it.

The Settings to enable when working in type script.

Sometimes it’s just nice to work with the bellow, self explenatory settings

  • 💡 noUnusedLocals
  • 💡 noUnusedParameters
  • 💡 noImplicitReturns

Advanced

We have dealt with function argument and return types, but there are a few more in-depth features we need to cover.

Collable Types

Both type aliases and interfaces offer the capability to describe call signatures:

interface TwoNumberCalculation {
  (x: number, y: number): number
}
type TwoNumberCalc = (x: number, y: number) => number
//(parameter) a: number
const add: TwoNumberCalculation = (a, b) => a + b
 
const subtract: TwoNumberCalc = (x, y) => x - y
//(parameter) x: number

The thing to not is that on the type Aliases we use => but for the interface we use : when defining our shape

Let’s pause for a minute to note:

  • The return type for an interface is :number, and for the type alias it’s => number
  • Because we provide types for the functions add and subtract, we don’t need to provide type annotations for each individual function’s argument list or return type

void

Sometimes functions don’t return anything, and we know from experience with JavaScript, what actually happens in the situation below is that x will be undefined:

function printFormattedJSON(obj: string[]) {
  console.log(JSON.stringify(obj, null, "  "))
}
const x = printFormattedJSON(["hello", "world"])
//const x: void

void is a special type, that’s specifically used to describe function return values. It has the following meaning:

The return value of a void function is intended to be ignored😎

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

function invokeInFourSeconds(callback: () => undefined) {
  setTimeout(callback, 4000)
}
function invokeInFiveSeconds(callback: () => void) {
  setTimeout(callback, 5000)
}
 
const values: number[] = []
invokeInFourSeconds(() => values.push(4))
//Type 'number' is not assignable to type 'undefined'.
invokeInFiveSeconds(() => values.push(4))

Void simply mean that the return type of this value should be ignore , thus do not use it anywhere .

Construct Signature

JavaScript functions can also be invoked with the new operator. TypeScript refers to these as constructors because they usually create a new object. You can write a construct signature by adding the new keyword in front of a call signature:

Construct signatures are similar to call signatures, except they describe what should happen with the new keyword.

interface DateConstructor {
  new (value: number): Date
}
 
let MyDateConstructor: DateConstructor = Date
const d = new MyDateConstructor()
// const d: Date

Function Overloads

Some JavaScript functions can be called in a variety of argument counts and types. For example, you might write a function to produce a Date that takes either a timestamp (one argument) or a month/day/year specification (three arguments).

In TypeScript, we can specify a function that can be called in different ways by writing overload signatures. To do this, write some number of function signatures (usually two or more), followed by the body of the function:

function makeDate(timestamp: number): Date
function makeDate(m: number, d: number, y: number): Date
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d)
  } else {
    return new Date(mOrTimestamp)
  }
}
const d1 = makeDate(12345678)
const d2 = makeDate(5, 5, 5)
const d3 = makeDate(1, 3)

This type

Sometimes we have a free-standing function that has a strong opinion around what this will end up being, at the time it is invoked.

<button onClick="myClickHandler">Click Me!</button>

We could define myClickHandler as follows

function myClickHandler(event: Event) {
this.disabled = true
// 'this' implicitly has type 'any' because it does not have a type annotation.
}
myClickHandler(new Event("click")) // seems ok

Objects type

You can define object types using type annotations:

let person: {
  name: string
  age: number
} = {
  name: "Alice",
  age: 30,
}

Interfaces:

Interfaces allow you to define the structure of an object:

interface Person {
  name: string
  age: number
}
 
let person: Person = {
  name: "Alice",
  age: 30,
}

Optional Properties:

You can mark properties as optional using ?:

function printCar(car: { make: string; model: string; year: number; chargeVoltage?: number }) {
  let str = `${car.make} ${car.model} (${car.year})`
  car.chargeVoltage
  //(property) chargeVoltage?: number | undefined
 
  if (typeof car.chargeVoltage !== "undefined") str += `// ${car.chargeVoltage}v`
  //we are sure that it is a number now.
  console.log(str)
}

Now we have set the property to be optional and you can or can not provide it and ts will be okay.

// Works
printCar({
  make: "Honda",
  model: "Accord",
  year: 2017,
})
// Also works
printCar({
  make: "Tesla",
  model: "Model 3",
  year: 2020,
  chargeVoltage: 220,
})
interface Employee {
  name: string
  role?: string
}
 
let alice: Employee = {
  name: "Alice",
}

Excess property checking

TypeScript helps us catch a particular type of problem around the use of object literals. Let’s look at the situation where the error arises:

function printCar(car: {
  make: string
  model: string
  year: number
  chargeVoltage?: number
}) {
  // implementation removed for simplicity
}
 
printCar({
  make: "Tesla",
  model: "Model 3",
  year: 2020,
  chargeVoltage: 220,
  color: "RED", // <0------ EXTRA PROPERTY
 

We will get an error such as Object literal may only specify known properties, and ‘color’ does not exist in type <the type the function expects>

})

Readonly Properties:

You can make properties read-only using the readonly modifier:

interface Point {
  readonly x: number
  readonly y: number
}
 
let origin: Point = { x: 0, y: 0 }
origin.x = 10 // Error, x is readonly

Index Signatures:

Sometimes we need to represent a type for dictionaries, where values of a consistent type are retrievable by keys. Let’s consider the following collection of phone numbers:

You can define objects with dynamic property names using index signatures:

interface Dictionary {
  [key: string]: number
}
 
let scores: Dictionary = {
  math: 90,
  science: 85,
}

Clearly it seems that we can store phone numbers under a “key” — in this case homeofficefax, and possibly other words of our choosing — and each phone number is comprised of three strings.

We could describe this value using what’s called an index signature:

const phones: {
  [k: string]: {
    country: string
    area: string
    number: string
  }
} = {}
 
phones.fax

Extending Interfaces:

Interfaces can extend other interfaces:

interface Person {
  name: string
}
 
interface Employee extends Person {
  role: string
}
 
let alice: Employee = {
  name: "Alice",
  role: "Developer",
}

Type Aliases:

You can use type aliases for object types as well:

type Point = {
  x: number
  y: number
}
 
let p: Point = { x: 1, y: 2 }

Advanced types

When it comes to programming languages and type systems, the concepts of structural and nominal types are crucial for understanding how types are defined and compared. Let’s dive into both of these concepts with examples and a deep explanation: Nominal Types: Nominal typing is based on the name or explicit declaration of a type. In languages with nominal typing, two types are considered distinct if they have different names, even if their structures are identical. This means that even if two types have the same structure, if they are defined using different names, they are treated as different types.

Structural Types: Structural typing, on the other hand, is based on the structure or shape of a type rather than its name. In languages with structural typing, two types are considered compatible if their structures match, regardless of their names. This enables more flexible and intuitive type compatibility.

// TypeScript example
interface Point {
  x: number
  y: number
}
 
function printPoint(pt: Point) {
  console.log(pt.x, pt.y)
}
 
const point = { x: 3, y: 7 }
printPoint(point) // Valid, even though it's not explicitly declared as a Point
 
// Or something like
 
class Car {
  make: string
  model: string
  year: number
  isElectric: boolean
}
 
class Truck {
  make: string
  model: string
  year: number
  towingCapacity: number
}
 
const vehicle = {
  make: "Honda",
  model: "Accord",
  year: 2017,
}
// Workds with all the above .
function printCar(car: { make: string; model: string; year: number }) {
  console.log(`${car.make} ${car.model} (${car.year})`)
}
 
printCar(new Car()) // Fine
printCar(new Truck()) // Fine
printCar(vehicle) // Fine

Comparison:

  • Nominal typing is more explicit and enforces type distinctions based on names. It can be helpful in preventing unintentional type mixing.
  • Structural typing is more flexible and allows for greater type reuse and compatibility based on the structure of types.

Type Alias

TypeScript provides two mechanisms for centrally defining types and giving them useful and meaningful names: Interfaces and type aliases.

A type alias is a way to create a custom name for an existing type or to define a complex type structure. This custom name can then be used throughout your codebase to make your type definitions more expressive and readable. Type aliases enhance the readability of complex type definitions and improve the maintainability of your code.

In TypeScript, the type keyword is used to create type aliases, which allow you to define custom names for existing types, unions, intersections, or other complex types. Type aliases make your code more readable, maintainable, and reusable by providing descriptive names for complex types or combinations of types.

Think back to the : {name: string, email: string} syntax we’ve used up until this point for type annotations.

This syntax will get increasingly complicated as more properties are added to this type. Furthermore, if we pass objects of this type around through various functions and variables, we will end up with a lot of types that need to be manually updated whenever we need to make any changes!

Type aliases help to address this, by allowing us to:

  • [p] define a more meaningful name for this type
  • [p] declare the particulars of the type in a single place
  • [p] import and export this type from modules, the same as if it were an exported value
///////////////////////////////////////////////////////////
 
// @filename: types.ts
 
export type UserContactInfo = {
  name: string
 
  email: string
}
 
///////////////////////////////////////////////////////////
// @filename: utilities.ts
import { UserContactInfo } from "./types"
 
/*
(alias) type UserContactInfo = { name: string; email: string; } import UserContactInfo
*/
function printContactInfo(info: UserContactInfo) {
  console.log(info)
 
  // (parameter) info: UserContactInfo
 
  console.log(info.email)
 
  //(property) email: string
}

Basic Usage:

You can use the type keyword to create a type alias:

type Age = number
let userAge: Age = 25

In this example, Age is a type alias for the number type. This makes your code more meaningful and self-documenting.

Custom Complex Types:

Type aliases can be used to create more complex types:

type Point = {
  x: number
  y: number
}
 
let origin: Point = { x: 0, y: 0 }

Here, Point is a type alias for an object with x and y properties.

type Point = {
  x: number
  y: number
}
 
type Address = {
  street: string
  city: string
  zipCode: string
}

Beauty of types

Bellow is our implementation of an employee object,

const employee: {
  readonly id: number
  name: string
  retired: (data: Date) => void
} = {
  id: 1,
  name: "Ilunga gisa",
  retired: (date: Date) => console.log(date),
}

There are three problems in this implementation

  • [c] If we want to create another employee we have to repeat the above shape
    • 🔥 which goes again the dry principle
  • [c] The second problem is that the other object might have other properties, which means that we are gonna have some kind of inconsistency
  • [c] Overall this structure is making our code a bit hard to understand

This is when we use a type alias, using a type alias we can define a custom type

type Employee = {
  readonly id: number
  name: string
  retired: (data: Date) => void
}
 
const employee: Employee = {
  id: 1,
  name: "Ilunga gisa",
  retired: (date: Date) => console.log(date),
}

Now we have a single place where we are defining our type and that is what makes the beauty of types, we can also reuse the type we defined

Here’s an example of how we can “cleaned up” an the code from our Union and Intersection Types section (previous chapter) through the use of type aliases:

///////////////////////////////////////////////////////////
 
// @filename: original.ts
 
/**
 
* ORIGINAL version
 
*/
 
export function maybeGetUserInfo():
  | ["error", Error]
  | ["success", { name: string; email: string }] {
  // implementation is the same in both examples
 
  if (Math.random() > 0.5) {
    return ["success", { name: "Mike North", email: "mike@example.com" }]
  } else {
    return ["error", new Error("The coin landed on TAILS :(")]
  }
}
 
///////////////////////////////////////////////////////////
 
// @filename: with-aliases.ts
 
type UserInfoOutcomeError = ["error", Error]
 
type UserInfoOutcomeSuccess = ["success", { name: string; email: string }]
 
type UserInfoOutcome = UserInfoOutcomeError | UserInfoOutcomeSuccess
 
/**
 
* CLEANED UP version
 
*/
 
export function maybeGetUserInfo(): UserInfoOutcome {
  // implementation is the same in both examples
 
  if (Math.random() > 0.5) {
    return ["success", { name: "Mike North", email: "mike@example.com" }]
  } else {
    return ["error", new Error("The coin landed on TAILS :(")]
  }
}

Inheritance

You can create type aliases that combine existing types with new behavior by using intersection (&) type

type SpecialDate = Date & { getReason(): string }
 
const newYearsEve: SpecialDate = {
  ...new Date(),
  getReason: () => "Last day of the year",
}
newYearsEve.getReason

While there’s no true extends keyword that can be used when defining type aliases, this pattern has a very similar effect

Interfaces type

Interfaces are a crucial feature in TypeScript that allow you to define contracts for the structure and behavior of objects. They provide a way to specify a set of properties and methods that a class must implement, promoting code consistency, reusability, and type safety.

An interface is a way of defining an object type. An “object type” can be thought of as, “an instance of a class could conceivably look like this”.

Where can they be used?

Since interfaces are used for defining the shape of your object, they can technically be used pretty much on everything you want to have an object shape kind of look (don’t mind the English ,I myself don’t know what I meant.)

For example, string | number is not an object type, because it makes use of the union type operator. an interface can not describe this.

interface UserInfo {
  name: string
  email: string
}
function printUserInfo(info: UserInfo) {
  info.name
 
(property) UserInfo.name: string
}

Defining Interfaces:

Interfaces are defined using the interface keyword. They outline the structure and methods that a class or object should adhere to.

interface Shape {
  calculateArea(): number
  color: string
}
 
class Circle implements Shape {
  radius: number
  color: string
 
  constructor(radius: number, color: string) {
    this.radius = radius
    this.color = color
  }
 
  calculateArea(): number {
    return Math.PI * this.radius * this.radius
  }
}

Inheritance in interfaces

  • 🔖 Just as in in JavaScript, a subclass extends from a base class.
    • 🔖 Additionally a “sub-interface” extends from a base interface, as shown in the example below
interface Animal {
isAlive(): boolean
}
interface Mammal extends Animal {
getFurOrHairColor(): string
}
interface Dog extends Mammal {
getBreed(): string
}
function careForDog(dog: Dog) {
dog.
    /* we have access to all of this.
      .getBreed
      .getFurOrHairColor
      .isAlive
      */
}
 

Properties in Interfaces:

  • ➡️ Interfaces can include property declarations. Classes implementing the interface must have properties matching the defined names and types.

Methods in Interfaces:

  • ➡️ Interfaces can include method signatures. Classes implementing the interface must provide implementations for the methods with matching signatures.

Optional Properties:

  • ➡️ You can mark properties as optional using the ? symbol.
interface Person {
  firstName: string
  lastName?: string // Optional property
}
const person1: Person = { firstName: "John" }
const person2: Person = { firstName: "Jane", lastName: "Doe" }

Read-only Properties:

  • ➡️ You can make properties read-only by using the readonly modifier.
interface Point {
  readonly x: number
  readonly y: number
}
 
const point: Point = { x: 10, y: 20 }
point.x = 30 // Error: Cannot assign to 'x' because it is a read-only property

Extending Interfaces:

  • Interfaces can extend other interfaces, allowing you to build upon existing contracts.

Implementing a class

TypeScript adds a second heritage clause that can be used to state that a given class should produce instances that confirm to a given interfaceimplements.

When working with classes and interfaces, you want to use the implement keyword

what it means bellow is that I have an interface that defines a method or property of my class

interface AnimalLike {
  eat(food): void
}
 
class Dog implements AnimalLike {
  /*
Class 'Dog' incorrectly implements interface 'AnimalLike'.
Property 'eat' is missing in type 'Dog' but required in type 'AnimalLike'.
*/
 
  bark() {
    return "woof"
  }
}

The above means make sure that my class has all the animalLike interface require! every instance of dog should make AnimalLike happy.

You can implement as many interfaces as you like.

class LivingOrganism {
  isAlive() {
    return true
  }
}
interface AnimalLike {
  eat(food): void
}
interface CanBark {
  bark(): string
}
 
class Dog extends LivingOrganism implements AnimalLike, CanBark {
  bark() {
    return "woof"
  }
  eat(food) {
    consumeFood(food)
  }
}

Union Types

Union types in TypeScript can be described using the | (pipe) operator. For example, if we had a type that could be one of two strings, "success" or "error", we could define it as

"success" | "error"

For example, the flipCoin() function will return "heads" if a number selected from (0, 1) is >= 0.5, or "tails" if <=0.5.

function flipCoin(): "heads" | "tails" {
  if (Math.random() > 0.5) return "heads"
  return "tails"
}
 
const outcome = flipCoin()

With union type we can give function parameters more than one type

function kg(weight: number | string): number {
  // using narrowing technique
  if (typeof weight === "number") return weight * 2.2
  else return parseInt(weight) * 2.2
}

Type aliases can be used to create union and intersection types:

type Status = "Pending" | "Approved" | "Rejected"
type CombinedPoint = Point & { label: string }

In this example, Status is a type alias for a string literal type, and CombinedPoint is a type alias for an object that combines properties of the Point type and an object with a label property.

Discriminative or Tagged union Type.

A discriminative union, also known as a tagged union or discriminated union, is a concept in TypeScript and other programming languages that involves creating a union type where each member of the union includes a specific discriminative property.

This property is used to distinguish between the different possible shapes of the union's instances.

function maybeGetUserInfo(): ["error", Error] | ["success", { name: string; email: string }] {
  if (Math.random() > 0.5) {
    return ["success", { name: "Mike North", email: "mike@example.com" }]
  } else {
    return ["error", new Error("The coin landed on TAILS :(")]
  }
}
 
/// ---cut---
 
const outcome = maybeGetUserInfo()
if (outcome[0] === "error") {
  // In this branch of your code, second is an Error
  outcome // ^?
} else {
  // In this branch of your code, second is the user info
  outcome // ^?
}

You can refactor the above using type aliases to me the code more readable

Example of another discriminative type union

interface Square {
  kind: "square"
  size: number
}
 
interface Circle {
  kind: "circle"
  radius: number
}
 
type Shape = Square | Circle
 
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "square":
      return shape.size * shape.size
    case "circle":
      return Math.PI * shape.radius * shape.radius
  }
}
 
const square: Shape = { kind: "square", size: 5 }
const circle: Shape = { kind: "circle", radius: 3 }
 
console.log(getArea(square)) // Output: 25
console.log(getArea(circle)) // Output: ~28.27

Intersection Types

Despite being substantially more rare, Intersection type allow you to combine multiple types into a single type that has all the properties and methods of the combined types.

This means that an intersection type includes the characteristics of all the types involved.

Sample combination.

Combining Object Types:

const person: Person = { name: "John", age: 30 }
const employee: Employee = { id: 1, jobTitle: "Developer" }
const personAndEmployee: PersonAndEmployee = { name: "Alice", age: 25, id: 2, jobTitle: "Manager" }

When you combine object types using an intersection type, the resulting type includes properties and methods from all the combined types.

Combining Interfaces:

You can also combine interfaces using intersection types.

interface Printable {
  print(): void
}
 
interface Loggable {
  log(): void
}
 
type LoggableAndPrintable = Printable & Loggable

Working with methods and properties

An intersection type inherits methods and properties from all the combined types.

const logAndPrint: LoggableAndPrintable = {
  print() {
    console.log("Printing...")
  },
  log() {
    console.log("Logging...")
  },
}

Combining Different Types

You can combine object types, interface types, and even primitive types using intersection types.

type Combined = Person & string & { isAdmin: boolean }

Intersection represents an object that is made of two or more things

type Draggable = {
  drag: () => void
}
 
type Resizable = {
  resize: () => void
}
 
type UIWidget = Draggable & Resizable
 
let textBox: UIWidget = {
  drag() {
    console.log("from drag type")
  },
  resize() {
    console.log("from resize type")
  },
}
type Person = {
  name: string
  age: number
}
 
type Employee = Person & {
  jobTitle: string
}
 
const employee: Employee = {
  name: "John",
  age: 30,
  jobTitle: "Developer",
}

Use case and limitation

Limitations:

While intersection types can be powerful, be cautious about using them excessively, as overly complex types can make your code less readable.

  • 🔥 Use Cases:
    • ➡️ Combining features from different types to create a new, comprehensive type.
    • ➡️ Creating types that are a mix of multiple concepts, such as a person who is also an employee.

Conclusion:

Function Type Aliases:

You can also use type aliases for function signatures:

type MathOperation = (x: number, y: number) => number
 
const add: MathOperation = (x, y) => x + y
const subtract: MathOperation = (x, y) => x - y

Here, MathOperation is a type alias for a function that takes two parameters of type number and returns a value of type number.

Generic Type Aliases:

Type aliases can be generic, allowing you to define flexible types:

type Container<T> = {
  value: T
}
 
let numberContainer: Container<number> = { value: 42 }
let stringContainer: Container<string> = { value: "Hello" }

Extending Type Aliases:

Type aliases can be extended to create new types based on existing ones:

type Person = {
  name: string
}
 
type Employee = Person & {
  role: string
}

Here, Employee extends the Person type with an additional role property.

Success #UseCases: Type aliases are particularly useful when you want to give meaningful names to complex types or type combinations. They improve code readability, make your intentions clearer, and help prevent errors by enforcing type consistency.

Type literal

sometimes we want to limit the values we can assign to a variable this is when we use the literal type

we want to define a quantity type which can either be 50 or 100?

let quantity: 50 = 50
// quantity can only hold
 
let q2: 50 | 100 = 100
// quantity can only be 50 or 100, nothing else.
 
// We can make this code even better using a custom alias!
type QT = 50 | 100
let quantiry: QT = 100

Literals can also be strings ! they don't have to be numbers only

type Metric = "cm" | "inch"

Comparison with Interfaces:

Type aliases and interfaces have similar purposes, but there are subtle differences.

Type Aliases:

Type aliases are more suitable for creating

  • union types,
  • intersection types,
  • and function type definitions.
  1. Compatible with Unions, Intersections, and Complex Types: Type aliases are more flexible when it comes to defining complex types, unions, intersections, and mapped types.
  2. Support for Generics: Type aliases can easily work with generic types, allowing you to create flexible and reusable components.
  3. Ability to Create Conditional Types: Type aliases can be used to create conditional types that modify the type based on certain conditions.
  4. Easier to Read and Understand: Type aliases often provide a clear and concise way to define complex types with meaningful names.

When to Use Type Aliases:

  • ➡️ Use type aliases when you need to create custom names for complex types, unions, intersections, or mapped types.
  • ➡️ Use them for working with generic types and conditional types.
  • ➡️ Use them when you want to define a descriptive and meaningful name for a complex type.
Interfaces

Interfaces are more suited for

  • defining object shapes
  • extending other interfaces.
  1. Augmentation: Interfaces can be extended or augmented using declaration merging, which allows you to add properties or methods to existing interface declarations.
  2. Class Implementation: Interfaces are often used to define contracts that classes must adhere to. A class that implements an interface must provide the specified properties and methods.
  3. Declaration Merging: Interfaces can be merged with other declarations of the same name, which is particularly useful when working with third-party libraries or global declarations.
  4. Better for Public APIs: Interfaces are generally preferred when defining public APIs for libraries or modules, as they provide a clear contract for consumers. When to Use Interfaces:
  • Use interfaces when you need to define the structure that classes or objects must adhere to.
  • Use them for extending or augmenting existing interfaces.
  • Use interfaces when creating contracts for public APIs or external modules.
  • Use them when you want to implement declaration merging to extend existing types.

Top and Bottom types

Defining types Let’s imagine that types describe a set of allowed values that a value might be.

const x: boolean

x could be either item from the following set {true, false}. Let’s look at another example:

const y: number

y could be any number. If we wanted to get technical and express this in terms of set builder notation, this would be {y | y is a number}

Let’s look at a few more, just for completeness:

let a: 5 | 6 | 7 // anything in { 5, 6, 7 }
let b: null // anything in { null }
let c: {
  favoriteFruit?: "pineapple" // { "pineapple", undefined }
  //(property) favoriteFruit?: "pineapple" | undefined
}

Top Types

A top type (symbol: ⊤) is a type that describes any possible value allowed by the system. To use our set theory mental model, we could describe this as {x| x could be anything }

TypeScript provides two of these types: any and unknown.

any

You can think of values with an any type as “playing by the usual JavaScript rules”. Here’s an illustrative example:

let flexible: any = 4
flexible = "Download some more ram"
flexible = window.document
flexible = setTimeout
flexible.it.is.possible.to.access.any.deep.property

It’s important to understand that any is not necessarily a problem — sometimes it’s exactly the right type to use for a particular situation.

This is an example of where you might want to use an any. in a console log.

console.log(window, Promise, setTimeout, "foo")
//(method) Console.log(...data: any[]): void

Practical use of top types

  • You will run into places where top types come in handy very often. In particular, if you ever convert a project from JavaScript to TypeScript, it’s very convenient to be able to incrementally add increasingly strong types. A lot of things will be any until you get a chance to give them some attention.
  • unknown is great for values received at runtime (e.g., your data layer). By obligating consumers of these values to perform some light validation before using them, errors are caught earlier, and can often be surfaced with more context.

Bottom Types

A bottom type (symbol: ⊥) is a type that describes no possible value allowed by the system. To use our set theory mental model, we could describe this as “any value from the following set: { } (intentionally empty)”

TypeScript provides one bottom type: never. At first glance, this may appear to be an extremely abstract and pointless concept, but there’s one use case that should convince you otherwise. Let’s take a look at this scenario below.

 

Narrowing with type guards

Introduction to Type Narrowing: Type narrowing is the process of refining or narrowing down the type of a variable based on specific conditions. It allows TypeScript to infer more precise types within conditional blocks, leading to improved type safety and better code understanding.

Type guards are expressions, which when used with control flow statement, allow us to have a more specific type for a particular value.

Why Type Narrowing is Important:

Type narrowing helps prevent errors by enabling the TypeScript compiler to understand the expected type of a variable in different branches of code, based on runtime checks. It improves code correctness and provides more accurate type information to developers.

Types of Type Narrowing:

Typeof Type guard

As we’ve seen, JavaScript supports a typeof operator which can give very basic information about the type of values we have at runtime. TypeScript expects this to return a certain set of strings:

  • "string" "number" "bigint" "boolean" "symbol" "undefined" "object" "function"
function printLength(value: string | number): void {
  if (typeof value === "string") {
    console.log("Length of string:", value.length)
  } else {
    console.log("Value is a number:", value)
  }
}

Truthiness narrowing

Truthiness might not be a word you’ll find in the dictionary, but it’s very much something you’ll hear about in JavaScript. In JavaScript, we can use any expression in conditionals, &&s, ||s, if statements, Boolean negations (!), and more. As an example, if statements don’t expect their condition to always have the type boolean.

function getUsersOnlineMessage(numUsersOnline: number) {
  if (numUsersOnline) {
    return `There are ${numUsersOnline} online now!`
  }
  return "Nobody's here. :("
}

Using built-in

// Some built-in functions
else if (Array.isArray(value)) {
  value
 
let value: [number]
}

Equality narrowing

TypeScript also uses switch statements and equality checks like =, !, ==, and != to narrow types. For example:

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    // We can now call any 'string' method on 'x' or 'y'.
    x.toUpperCase();
 
(method) String.toUpperCase(): string
    y.toLowerCase();
 
(method) String.toLowerCase(): string
  } else {
    console.log(x);
 
(parameter) x: string | number
    console.log(y);
 
(parameter) y: string | boolean
  }
}

The in operator narrowing

JavaScript has an operator for determining if an object or its prototype chain has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types.

For example, with the code: “value” in x. where “value” is a string literal and x is a union type. The “true” branch narrows x’s types which have either an optional or required property value, and the “false” branch narrows to types which have an optional or missing property value.

type Fish = { swim: () => void }
type Bird = { fly: () => void }
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    return animal.swim()
  }
 
  return animal.fly()
}

To reiterate, optional properties will exist in both sides for narrowing. For example, a human could both swim and fly (with the right equipment) and thus should show up in both sides of the in check:

type Fish = { swim: () => void }
 
type Bird = { fly: () => void }
 
type Human = { swim?: () => void; fly?: () => void }
 
function move(animal: Fish | Bird | Human) {
  if ("swim" in animal) {
    animal
 
    //(parameter) animal: Fish | Human
  } else {
    animal
 
    //(parameter) animal: Bird | Human
  }
}

Instanceof Type Guard:

The instanceof operator can be used to narrow down the type of an object based on its constructor function.

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
 
function soundOfAnimal(animal: Animal): void {
  if (animal instanceof Dog) {
    console.log("Woof!")
  } else if (animal instanceof Cat) {
    console.log("Meow!")
  }
}

Assignments

As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately.

let x = Math.random() < 0.5 ? 10 : "hello world!"
 
let x: string | number
x = 1
 
console.log(x)
 
let x: number
x = "goodbye!"
 
console.log(x)
 
let x: string

Custom Type Predicates:

We’ve worked with existing JavaScript constructs to handle narrowing so far, however sometimes you want more direct control over how types change throughout your code.

To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:

function isString(value: any): value is string {
  return typeof value === "string"
}
function processValue(value: unknown): void {
  if (isString(value)) {
    console.log("Value is a string:", value.toUpperCase())
  }
}

Type Assertions:

Type assertions are a way to manually narrow down the type of a variable when you know more about its type than TypeScript can infer.

let value: unknown = "hello"
let length: number = (value as string).length

Discriminated union

Most of the examples we’ve looked at so far have focused around narrowing single variables with simple types like string, boolean, and number. While this is common, most of the time in JavaScript we’ll be dealing with slightly more complex structures.

For some motivation, let’s imagine we’re trying to encode shapes like circles and squares. Circles keep track of their radiuses and squares keep track of their side lengths. We’ll use a field called kind to tell which shape we’re dealing with. Here’s a first attempt at defining Shape.

interface Shape {
  kind: "circle" | "square"
  radius?: number
  sideLength?: number
}

Notice we’re using a union of string literal types: “circle” and “square” to tell us whether we should treat the shape as a circle or square respectively. By using “circle” | “square” instead of string, we can avoid misspelling issues.

function handleShape(shape: Shape) {
  // oops!
  if (shape.kind === "rect") {
    // This comparison appears to be unintentional because the types '"circle" | "square"' and '"rect"' have no overlap.
    // ...
  }
}

We can write a getArea function that applies the right logic based on if it’s dealing with a circle or square. We’ll first try dealing with circles.

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2
  //'shape.radius' is possibly 'undefined'.
}

User Defined Type Guards

If we lived in a world where we only had the type guards we’ve seen so far, we’d quickly run into problems as our use of built-in type guards become more complex.

For example, how would we validate objects that are type-equivalent with our CarLike interface below?

interface CarLike {
  make: string
  model: string
  year: number
}
 
let maybeCar: unknown
 
// the guard
if (
  maybeCar &&
  typeof maybeCar === "object" &&
  "make" in maybeCar &&
  typeof maybeCar["make"] === "string" &&
  "model" in maybeCar &&
  typeof maybeCar["model"] === "string" &&
  "year" in maybeCar &&
  typeof maybeCar["year"] === "number"
) {
  maybeCar
 
  // let maybeCar: object❌
}

Validating this type might be possible, but it would almost certainly involve casting.

Even if this did work, it is getting messy enough that we’d want to refactor it out into a function or something, so that it could be reused across our codebase.

interface CarLike {
  make: string
  model: string
  year: number
}
 
let maybeCar: unknown
 
// the guard
function isCarLike(valueToTest: any) {
  return (
    valueToTest &&
    typeof valueToTest === "object" &&
    "make" in valueToTest &&
    typeof valueToTest["make"] === "string" &&
    "model" in valueToTest &&
    typeof valueToTest["model"] === "string" &&
    "year" in valueToTest &&
    typeof valueToTest["year"] === "number"
  )
}
// using the guard
if (isCarLike(maybeCar)) {
  maybeCar
  //let maybeCar: unknown❌
}

Even after doing that delusional type checking mybeCar is still unknown ! despite all we did, it was not enough to tell typescript that mybeCar is actually a carLIke Object!

The solution is simply user defined type.

value is Foo

The first kind of user-defined type guard we will review is an is type guard. It is perfectly suited for our example above because it’s meant to work in cooperation with a control flow statement of some sort, to indicate that different branches of the “flow” will be taken based on an evaluation of valueToTest’s type.

Pay very close attention to isCarLike’s return type

interface CarLike {
  make: string
  model: string
  year: number
}
 
let maybeCar: unknown
 
// the guard
function isCarLike(valueToTest: any): valueToTest is CarLike {
  return (
    valueToTest &&
    typeof valueToTest === "object" &&
    "make" in valueToTest &&
    typeof valueToTest["make"] === "string" &&
    "model" in valueToTest &&
    typeof valueToTest["model"] === "string" &&
    "year" in valueToTest &&
    typeof valueToTest["year"] === "number"
  )
}
 
// using the guard
if (isCarLike(maybeCar)) {
  maybeCar
 
  //let maybeCar: CarLike✅
}

asserts value is Foo

There is another approach we could take that eliminates the need for a conditional. Pay very close attention to assertsIsCarLike’s return type:

interface CarLike {
  make: string
  model: string
  year: number
}
 
let maybeCar: unknown
 
// the guard
function assertsIsCarLike(valueToTest: any): asserts valueToTest is CarLike {
  if (
    !(
      valueToTest &&
      typeof valueToTest === "object" &&
      "make" in valueToTest &&
      typeof valueToTest["make"] === "string" &&
      "model" in valueToTest &&
      typeof valueToTest["model"] === "string" &&
      "year" in valueToTest &&
      typeof valueToTest["year"] === "number"
    )
  )
    throw new Error(`Value does not appear to be a CarLike${valueToTest}`)
  // If this function finishes without throwing, then you are indeed car like
}
 
// using the guard
maybeCar
// let maybeCar:unkown
 
assertsIsCarLike(maybeCar)
maybeCar
//let maybeCar:CarLike

With the asert the code translate to, if you pass through the function and we haven't thrown, you are car like

Conceptually, what’s going on behind the scenes is very similar. By using this special syntax to describe the return type, we are informing TypeScript that if assertsIsCarLike throws an error, it should be taken as an indication that the valueToTest is NOT type-equivalent to CarLike.

Nullish values

There are situations where we have to plan for, and deal with the possibility that values are null or undefined

Type narrowing is a powerful concept that allows you to refine the type of a variable based on certain conditions. It’s essential for achieving better type safety and making your code more precise.

Although null, void and undefined are all used to describe “nothing” or “empty”, they are independent types in TypeScript. Learning to use them to your advantage, and they can be powerful tools for clearly expressing your intent as a code author.

null

null means: there is a value, and that value is nothing. While some people believe that null is not an important part of the JS language, I find that it’s useful to express the concept of a “nothing” result (kind of like an empty array, but not an array).

This nothing is very much a defined value and is certainly a presence not an absence of information.

const userInfo = {
  name: "Mike",
  email: "mike@example.com",
  secondaryEmail: null, // user has no secondary email
}

Undefined

Undefined means the value isn’t available (Yet?) In the example below completed at will be a set at some point but there is a period of time when we haven’t yet set it.

const formInProgress = {
  createdAt: new Date(),
  data: new FormData(),
  completedAt: undefined, //
}
function submitForm() {
  formInProgress.completedAt = new Date()
}

void

We have already covered this in the functions chapter, but as a reminder:

void should exclusively be used to describe that a function’s return value should be ignored

console.log(`console.log returns nothing.`)
//(method) Console.log(...data: any[]): void

Non-null Assertion Operator

The non-null assertion operator (!.) is used to cast away the possibility that a value might be null or undefined.

Keep in mind that the value could still be null or undefined, this operator just tells TypeScript to ignore that possibility.

type GroceryCart = {
  fruits?: { name: string; qty: number }[]
  vegetables?: { name: string; qty: number }[]
}
 
const cart: GroceryCart = {}
 
cart.fruits.push({ name: "kumkuat", qty: 1 })
Object is possibly 'undefined'.
 /*
(property) fruits?: {
    name: string;
    qty: number;
}[] | undefined
*/
cart.fruits!.push({ name: "kumkuat", qty: 1 })

By default null and undefined are not enabled and you should check that in the config files to allow them

function greet(name: string | null | undefined) {
  if (name) console.log(name.toUpperCase())
  else console.log("holla")
}
 
// Sinse we used union type for the edge cases of null and undefined
greet(undefined) // holla
greet(null) // holla

Optional chaining

When working with nullable objects, we often want to do null checks, and for this, optional chaining is our go to

type Customer = {
  birthday?: Date // Might or might not be there
}
 
function getCustomer(id: number): Customer | null | undefined {
  // function might return the customer type, null or undefined
  return id === 0 ? null : { birthday: new Date() } // if we have an id then return a birtday
}
 
let customer = getCustomer(0)
console.log(customer?.birthday?.getFullYear())
// Use optional chaining to make sure
Optional element access operator

Useful when we are dealing with array, we might have an array of customers, and we want to print the first customer on the console. customers[0]

If the array is null or undefined we must do a check first

if(customers!==null && customers !=== undefines)
	customers[0]

This is when we might want to use the optional element access operator.

customers?.[0]

Yes, we also have an optional call

let log: any = (message: string) => console.log(message);
log.("a");

But the below would surely produce an error because our function is null

let log: any = null
log.("a");

This is where we might want to use the optional call

let log: any = null
log?.("a")

Null Coalescing Operator

let speed: number | null = null
let ride = {
  speed: speed ?? 30,
}

Type Assertions

Type assertions (also known as type casting) are a way to tell the TypeScript compiler that you, as a developer, have more information about the type of a value than TypeScript can infer on its own. Type assertions allow you to specify a type for a value, essentially telling TypeScript to trust your judgment.

let's say we want to select an input element on our page using the getElementById, this in typescript will be inferred to an htmlElement which should instead return an htmlInputElement

let phone = document.getElementById("para")
// In TS it looks like this:
// let phone: HTMLElement | null  = document.getElementById("para");

This is when we might want to tell TS that we know more about our type that it does. we can achieve this using the as keyword

let phone = document.getElementById("inpPhone") as HTMLInputElement
// Or alternatively you can use the <> syntax
let phone = <HTMLInputElement>document.getElementById("inpPhone")

Using the as Syntax:

You can use the as syntax to assert a value’s type:

let value: any = "Hello"
let length: number = (value as string).length

Here, (value as string) tells TypeScript to treat value as a string type temporarily.

Using the Angle Bracket Syntax (Alternative):

An alternative syntax for type assertions is the angle bracket syntax:

let length: number = (<string>value).length

Both syntaxes achieve the same result, but the as syntax is generally recommended, especially when using JSX (if you're using React).

When to Use Type Assertions:

Type assertions should be used carefully, and only when you have a strong reason to believe that you know the type of a value better than TypeScript's type inference. It's important to note that type assertions don't perform any runtime checks or conversions; they only affect how TypeScript analyzes the code.

Avoiding Type Assertion Conflicts:

Be cautious when using type assertions with the any type. Since any disables type checking, assertions might lead to unexpected behavior if not used correctly:

let value: any = "Hello"
let length: number = (value as string).length
// Be careful when asserting `any` types

Asserting to Union Types:

You can use type assertions to narrow down union types:

function printText(text: string | number): void {
  if (typeof text === "string") {
    console.log(text.toUpperCase())
  } else {
    console.log(text.toFixed(2))
  }
}

Here, the typeof text === "string" type assertion narrows down the type of text.

User-Defined Type Guards:

Type assertions can be combined with user-defined type guards to achieve more precise type narrowing:

function isString(value: any): value is string {
  return typeof value === "string"
}
 
let value: any = "Hello"
 
if (isString(value)) {
  console.log(value.toUpperCase())
}

In this example, the isString function serves as a type guard, and the value is string assertion is used to narrow down the type.

Success Using as with JSX:

If you’re using TypeScript with JSX (for frameworks like React), it’s recommended to use the as syntax for type assertions, as the angle bracket syntax can conflict with JSX syntax.

The unknown type

The unknown type is a safer alternative to the any type in TypeScript. It represents a value about which the TypeScript compiler doesn’t know anything regarding its type.

Unlike any, you can't perform arbitrary operations on an unknown value without narrowing its type first.

Type Safety:

The unknown type is designed to ensure type safety while working with values of uncertain types. It prevents you from accidentally performing operations that might lead to runtime errors due to incompatible types.

Type Checking:

When you have a value of type unknown, you can’t directly access its properties or call its methods without first narrowing the type using type assertions, type guards, or other techniques.

Type narrowing

You often need to narrow down the unknown type to a specific type before using it:

function processValue(value: unknown): void {
  if (typeof value === "string") {
    console.log(value.toUpperCase())
  } else if (typeof value === "number") {
    console.log(value.toFixed(2))
  }
}

In this example, the type of value is narrowed down within the conditions.

Type Assertions with unknown:

You can use type assertions to tell TypeScript about the actual type of an unknown value:

let value: unknown = "Hello"
let length: number = (value as string).length

Avoidance of Type Errors:

Using unknown instead of any helps you catch type-related errors early in your codebase. It enforces a stronger type checking process and promotes safer programming practices.

Compatibility:

The unknown type is compatible with all other types in TypeScript. You can assign an unknown value to any other type, but you need to handle type checks and narrowing before using it.

Difference from any:

Unlike any, which disables type checking entirely, unknown forces you to perform type checks and narrow down the type before performing operations on the value.

Safe APIs

The unknown type encourages you to create APIs that are safe to use with values of uncertain types. This leads to more robust and maintainable code.

Use Cases:

Use the unknown type when you’re dealing with values from dynamic sources, such as user inputs or external APIs, and you want to ensure strong type checking without sacrificing flexibility.

The never type

The never type in TypeScript represents values that will never occur. It’s used to indicate that a function will not return normally or that a variable cannot have any value.

The never type is often used in scenarios where an operation leads to an error, an exception is thrown, or an infinite loop is encountered.

Functions Returning never:

A function returning never is one that throws an exception or enters an infinite loop. Such functions are used to indicate that they don’t produce any meaningful value and will never complete normally.

function throwError(message: string): never {
  throw new Error(message)
}
 
function infiniteLoop(): never {
  while (true) {
    // Infinite loop
  }
}

Narrowing Unreachable Code:

The never type is used by TypeScript to narrow types in unreachable code:

function unreachableCodeExample(x: string | number) {
  if (typeof x === "string") {
    x // In this block, x is narrowed to string
  } else if (typeof x === "number") {
    x // In this block, x is narrowed to number
  } else {
    x // In this block, x is narrowed to never
  }
}

Exhaustive Type Checking:

When using the never type in exhaustive type checking, you can ensure that all possible cases have been handled:

type Fruit = "Apple" | "Banana" | "Orange"
 
function getFruitColor(fruit: Fruit): string {
  switch (fruit) {
    case "Apple":
      return "Red"
    case "Banana":
      return "Yellow"
    case "Orange":
      return "Orange"
    default:
      const exhaustiveCheck: never = fruit
      throw new Error(`Unhandled fruit: ${exhaustiveCheck}`)
  }
}

Variables with No Possible Value:

Variables that are assigned to the result of functions that never return, or to conditions that are always false, will have the never type:

let impossibleValue: never
 
if (false) {
  impossibleValue = "This will never happen"
}
 
function neverReturningFunction(): never {
  throw new Error("This function never returns")
}
 
impossibleValue = neverReturningFunction()

Difference from void:

The void type indicates that a function doesn’t return a value, while the never type indicates that a function doesn’t return normally and has no reachable end point.

Use Case

Use the never type when you want to indicate that a function will throw an exception, get into an infinite loop, or that a variable can’t have any possible value.

Object Oriented with typescript.

Defining and Creating Classes

A class in TypeScript is defined using the class keyword. It acts as a blueprint for creating objects with shared characteristics.

// Define a class named "Person"
class Person {
  // Properties of the class
  firstName: string
  lastName: string
 
  // Constructor to initialize properties
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
  }
 
  // Method to greet the person
  greet() {
    console.log(`Hello, ${this.firstName} ${this.lastName}!`)
  }
}
 
// Create an instance of the "Person" class
const alice = new Person("Alice", "Smith")
// Call the "greet" method
alice.greet() // Output: "Hello, Alice Smith!"

In this example, the Person class has properties firstName and lastName, a constructor to initialize them, and a method greet() to display a greeting.

Properties and Methods

Properties store data within a class, while methods define the behavior of the class. TypeScript allows you to specify types for both.

class Account {
  id: number
  owner: string
  balance: number // Creating our construcot
  constructor(id: number, owner: string, balance: number) {
    this.id = id
    this.owner = owner
    this.balance = balance
  }
  // Defining our method
  deposit(amount: number) {
    if (amount <= 0) throw new Error("Invalid amount")
    this.balance += amount
  }
}

Read only and optional properties

Read only property

In Typescript we have modifiers that we can make use of to write robust codes. le’t say we don’t want the id of the bank account to never change

class Account {
  readonly id: number // Read only property, you can't change it at least in typescript 🤣.
  owner: string
  balance: number
  secret?: string // Might or might not be there
  // Creating our construcot
  constructor(id: number, owner: string, balance: number) {
    this.id = id
    this.owner = owner
    this.balance = balance
  }
}

Optional Label

sometimes you want something optional that might or might not be in your class. for that you use the ?

class Account {
  id: number;
  secret?: string;
  // Creating our construcot
  constructor(id: number) {
    this.id = id;
  }

Creating an object or instance.

Once you’ve defined the class, you can create an instance (object) of that class using the new keyword followed by the class constructor and any necessary arguments.

const account = new Account(23, "Ilunga Gisa Daniel", 412_003_400)

Check for instance

sometimes you need to check whether an object is an instance of something or not and you can use the instanceof syntax

console.log(account instanceof Account)

Access Object Properties and Methods:

After creating the object, you can access its properties and methods using the dot notation.

console.log(alice.firstName) // Output: "Alice" console.log(alice.lastName);  // Output: "Smith" alice.greet();                // Output: "Hello, Alice Smith!"

You can use the instance variable (in this case, alice) to access the properties and methods defined within the class.

Access Modifier

ccess modifiers in TypeScript are keywords that determine the visibility and accessibility of class members (properties and methods) from different parts of your code. TypeScript provides three main access modifiers: public, private, and protected.

Public

  • public Access Modifier:
    • Members marked as public are accessible from anywhere, both within the class and from external code.
    • It’s the default access modifier if none is specified.
class Person {
  public firstName: string
 
  constructor(firstName: string) {
    this.firstName = firstName
  }
}
 
const alice = new Person("Alice")
console.log(alice.firstName) // Accessible

Private

  • private Access Modifier:
    • Members marked as private are only accessible within the class they’re defined in.
    • They can’t be accessed from outside the class, including subclasses.
class Person {
  private socialSecurityNumber: string
 
  constructor(ssn: string) {
    this.socialSecurityNumber = ssn
  }
 
  getSSN(): string {
    return this.socialSecurityNumber
    // Accessible within the class
  }
}
 
const alice = new Person("123-45-6789")
// console.log(alice.socialSecurityNumber); // Error: private member

Protected

  • protected Access Modifier:
    • Members marked as protected are accessible within the class they’re defined in and its subclasses (derived classes).
    • They can’t be accessed from outside the class hierarchy.
class Vehicle {
  protected speed: number
 
  constructor(speed: number) {
    this.speed = speed
  }
}
 
class Car extends Vehicle {
  accelerate() {
    this.speed += 10 // Accessible within subclasses
  }
}
 
const myCar = new Car(60)
// console.log(myCar.speed); // Error: protected member

Access modifiers provide encapsulation, allowing you to control how class members are accessed and manipulated from different parts of your code. Using the appropriate access modifier helps ensure that your classes maintain their intended behavior and data integrity.

Parameter Property

Parameter properties in TypeScript are a concise way to declare and initialize class properties directly within the constructor parameters. This feature simplifies the process of defining and initializing properties by combining the parameter declaration and property assignment into a single step.

class Person {
  constructor(
    public firstName: string,
    public lastName: string,
  ) {
    // The "public" access modifier before the parameters automatically creates and initializes properties.
    // This is equivalent to declaring "firstName: string; lastName: string;" above the constructor.
  }
 
  greet() {
    console.log(`Hello, ${this.firstName} ${this.lastName}!`)
  }
}
 
const alice = new Person("Alice", "Smith")
alice.greet() // Output: "Hello, Alice Smith!"
console.log(alice.firstName) // Accessible due to public parameter property

In this example, the public access modifier before the constructor parameters firstName and lastName creates and initializes corresponding properties within the class. This results in shorter and more readable code compared to declaring properties separately and initializing them in the constructor.

Parameter properties can also use other access modifiers ( private, protected, or no modifier), similar to regular class properties.

class Book {
  constructor(
    private title: string,
    protected author: string,
  ) {
    // The "private" and "protected" modifiers work similarly for parameter properties.
  }
 
  displayInfo() {
    console.log(`Title: ${this.title}, Author: ${this.author}`)
  }
}
 
const myBook = new Book("The Catcher in the Rye", "J.D. Salinger")
myBook.displayInfo() // Output: "Title: The Catcher in the Rye, Author: J.D. Salinger"
// console.log(myBook.title); // Error: private member
// console.log(myBook.author); // Error: protected member

Using parameter properties can help you create more concise and maintainable code by reducing redundancy and ensuring that properties are correctly initialized when objects are created.

Getter and Setter

In TypeScript, getters and setters are special methods that allow you to define how the properties of a class are accessed and modified. Getters are used to retrieve the value of a property, and setters are used to assign a new value to a property. They provide a way to encapsulate and control access to class properties, enabling you to add additional logic or validation when getting or setting values.

class Rectangle {
  private _width: number
  private _height: number
 
  constructor(width: number, height: number) {
    this._width = width
    this._height = height
  }
 
  // Getter for width property
  get width(): number {
    return this._width
  }
 
  // Setter for width property
  set width(value: number) {
    if (value > 0) {
      this._width = value
    }
  }
 
  // Getter for height property
  get height(): number {
    return this._height
  }
 
  // Setter for height property
  set height(value: number) {
    if (value > 0) {
      this._height = value
    }
  }
 
  // Method to calculate area
  calculateArea(): number {
    return this._width * this._height
  }
}
 
const rect = new Rectangle(10, 5)
console.log(rect.calculateArea()) // Output: 50
 
rect.width = 12 // Using the setter
rect.height = 6 // Using the setter
 
console.log(rect.width) // Using the getter
console.log(rect.height) // Using the getter
console.log(rect.calculateArea()) // Output: 72

Index Signature

Index signatures in TypeScript allow you to define a "key" and the corresponding "value" type for properties that are not known beforehand.

They enable you to create objects that behave like dictionaries or maps, where you can use arbitrary keys to access values. Index signatures are particularly useful when dealing with objects that have dynamic keys or when working with JSON-like structures.

This just won't work in typescript

let person = {}
person.name = "a"

Here's how you can use index signatures in TypeScript:

// Defining a type with an index signature
type Dictionary = {
  [key: string]: number
  // The key is a string, and the value is a number
}
 
// Creating an object using the Dictionary type
const scores: Dictionary = {
  alice: 90,
  bob: 85,
  carol: 95,
}
 
console.log(scores.alice) // Output: 90
console.log(scores["bob"]) // Output: 85
 
// Adding new entries using the index signature
scores.dave = 88
scores["emily"] = 92
 
console.log(scores.dave) // Output: 88
console.log(scores["emily"]) // Output: 92

In this example, the Dictionary type has an index signature [key: string]: number, which means you can use any string key to access a corresponding number value. You can use both dot notation (object.key) and bracket notation (object["key"]) to access the properties defined by the index signature.

Keep in mind the following points when using index signatures:

  • Index signatures can have other properties along with them, but their types must be compatible with the index signature.
  • You can use a single index signature per object type.
  • While index signatures provide flexibility, they don’t provide type safety or accurate IntelliSense for the keys and values like explicitly typed properties.

Index signatures are a powerful feature in TypeScript for dealing with dynamic data structures and scenarios where you need to work with objects with varying keys.

Class Index signature

You can use index signatures in a class in a similar way to how you use them in regular objects. Here’s how you can implement index signatures within a class:

class Dictionary {
  private data: { [key: string]: number } = {} // Private property for storing data
 
  // Method to set a value using the index signature
  setValue(key: string, value: number): void {
    this.data[key] = value
  }
 
  // Method to get a value using the index signature
  getValue(key: string): number | undefined {
    return this.data[key]
  }
}
 
const scores = new Dictionary()
scores.setValue("alice", 90)
scores.setValue("bob", 85)
scores.setValue("carol", 95)
 
console.log(scores.getValue("alice")) // Output: 90
console.log(scores.getValue("bob")) // Output: 85
 
scores.setValue("dave", 88)
scores.setValue("emily", 92)
 
console.log(scores.getValue("dave")) // Output: 88
console.log(scores.getValue("emily")) // Output: 92

Static members

Static members in TypeScript are members (properties or methods) that belong to the class itself rather than to instances of the class. These members are shared across all instances of the class and can be accessed using the class name. Static members are often used for utility functions, constants, or methods that don’t require instance-specific data.

Here's how you can define and use static members in TypeScript:

class MathOperations {
  static PI: number = 3.14159 // Static property
 
  static calculateArea(radius: number): number {
    return this.PI * radius * radius
  }
}
 
console.log(MathOperations.PI) // Accessing static property
console.log(MathOperations.calculateArea(5)) // Calling static method

In this example, the MathOperations class has a static property PI and a static method calculateArea(). These members are associated with the class itself, not with instances of the class. You can access them using the class name followed by the member name.

In this example, the static method createInstance() is used to create instances of the Counter class, and the static method getCount() is used to retrieve the count of instances created.

Imagine you're building a simple class to represent Uber rides. You want to keep track of the total number of rides across all instances of the class. This is a perfect scenario for using a static property and a static method.

class UberRide {
  static totalRides: number = 0 // Static property to track total rides
  startLocation: string
  endLocation: string
 
  constructor(startLocation: string, endLocation: string) {
    this.startLocation = startLocation
    this.endLocation = endLocation
    UberRide.totalRides++ // Increment the total rides whenever a new ride is created
  }
 
  // Instance method to display ride details
  displayRideDetails() {
    console.log(`Ride from ${this.startLocation} to ${this.endLocation}`)
  }
 
  // Static method to get the total rides
  static getTotalRides(): number {
    return UberRide.totalRides
  }
}
 
const ride1 = new UberRide("Home", "Office")
const ride2 = new UberRide("Gym", "Park")
 
console.log(ride1.displayRideDetails()) // Output: Ride from Home to Office
console.log(ride2.displayRideDetails()) // Output: Ride from Gym to Park
 
console.log(UberRide.getTotalRides()) // Output: 2 (Total number of rides)

Inheritance

Inheritance is a fundamental concept in object-oriented programming (OOP) that allows you to create a new class based on an existing class. The new class, known as the “subclass” or “derived class,” inherits properties and behaviors from the existing class, known as the “base class” or “parent class.” TypeScript supports inheritance, enabling you to create class hierarchies and promote code reusability.

Here's how you can implement inheritance in TypeScript:

// Base class (parent class)
class Animal {
  name: string
 
  constructor(name: string) {
    this.name = name
  }
 
  // Method in the base class
  makeSound() {
    console.log("Animal makes a sound")
  }
}
 
// Subclass (derived class)
class Dog extends Animal {
  breed: string
 
  constructor(name: string, breed: string) {
    super(name) // Call the base class constructor using "super"
    this.breed = breed
  }
 
  // Method in the subclass
  makeSound() {
    console.log("Dog barks")
  }
}
 
// Creating instances of the classes
const animal = new Animal("Generic Animal")
const dog = new Dog("Buddy", "Golden Retriever")
 
console.log(animal.name) // Output: Generic Animal
animal.makeSound() // Output: Animal makes a sound
 
console.log(dog.name) // Output: Buddy
console.log(dog.breed) // Output: Golden Retriever
dog.makeSound() // Output: Dog barks

Key points to remember about inheritance in TypeScript:

  • ➡️ The extends keyword is used to create a subclass that inherits from a base class.
  • ➡️ The super keyword is used to call the constructor of the base class from within the subclass constructor.
  • ➡️ Subclasses can override methods of the base class to provide their own implementation.
  • ➡️ Subclasses can also introduce new properties and methods in addition to those inherited from the base class.
  • ➡️ An instance of a subclass is also an instance of the base class, but the reverse is not true.
  • ➡️ TypeScript supports single inheritance, meaning a subclass can extend only one base class.

Inheritance is a powerful tool for organizing and structuring your code, promoting code reuse, and creating class hierarchies that represent relationships between different types of objects.

Method Overriding

Method overriding is a core concept in object-oriented programming that allows a subclass to provide a specific implementation for a method that is already defined in its parent class. In TypeScript, you can override methods inherited from a base class by providing a new implementation in the subclass. This enables you to customize the behavior of a method for specific types of objects.

class Person {
  constructor(
    public firstName: string,
    public lastName: string,
  ) {}
  talk() {
    console.log("I do speak ")
  }
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
}
 
class Student extends Person {
  dob: string = "July 22 1990"
  //override and completely change the implementation
  override talk(): void {
    console.log("I can not speak NO NO NO")
  }
}
 
class Teacher extends Person {
  // Override and extend from what we had
  override get fullName() {
    return `Prof. ${super.fullName}`
  }
}
 
const student = new Student("Ilunga Gisa", "Daniel")
const teacher = new Teacher("Mark", "Jackson")
console.log(student.fullName) // Ilunga Gisa Daniel
console.log(teacher.fullName) // Prof. Mark Jackson

Polymorphism

Polymorphism is a fundamental concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass. It enables you to work with objects in a more generic way, allowing you to write code that can work with various subclasses without knowing their specific types.

In TypeScript, polymorphism is achieved through inheritance and method overriding. Let's use an example to demonstrate polymorphism in TypeScript

// Let's say we have an array of object and we would like to iterate over it and simply output the fullName.
 
printNames([new Student("John", "Doe"), new Teacher("Jack", "Jameson")])
// Looping over our objects to ouput the fullName
function printNames(people: Person[]) {
  for (let person of people) {
    console.log(person.fullName)
  }
}
;`ouput:
John Doe
Prof. Jack Jameson
>The point is, we did not have to change our implementation for the overriden objects, our loops workds and what is being outputed is dynamic to the implementations of our classes.`
 
// We can eve add another class and frankly we won't have to modify our implementation.
class Principal extends Person {
  // Override and extend from what we had
  override get fullName() {
    return `Principal. ${super.fullName}`
  }
}

We don't have to change our function for our program to work, everything is dynamic and takes another form as you implemented it, and that is polymorphism in action

Open close principle

The Open/Closed Principle (OCP) is one of the SOLID principles of object-oriented design, introduced by Bertrand Meyer. It emphasizes that software entities (such as classes, modules, functions) should be open for extension but closed for modification. In other words, once a module or class is defined and implemented, it should not be modified to add new features. Instead, the module should be extended to accommodate new functionalities.

  • [/] Open for Extension:

    • ➡️ You should be able to add new features or behaviors to a module or class without altering its existing implementation.
  • [/] Closed for Modification:

    • ➡️ Once a module or class is implemented, its code should not be modified to add new features. Instead, new features should be added through extension.

Abstract Classes and Methods

Abstract classes and methods are advanced features in TypeScript that allow you to define the structure of a class without providing a complete implementation. Abstract classes act as blueprints that other classes can extend, while abstract methods define method signatures that must be implemented by subclasses. They’re particularly useful for creating a common interface for a group of related classes.

Abstract Classes:

  • ➡️ An abstract class is a class that cannot be instantiated on its own. It serves as a base for other classes to inherit from.
  • ➡️ Abstract classes can have properties, methods (including abstract methods), and constructors.
  • ➡️ Abstract classes are defined using the abstract keyword before the class keyword.
  • ➡️ Abstract classes can contain both concrete (implemented) methods and abstract (unimplemented) methods.
abstract class Shape {
  constructor(public color: string) {}
  abstract render(): void // Our abstract method that needs to be implemented in our child class.
  // Abstract method can only be implemented in our abstract class
}
 
class Circle extends Shape {
  // Creating the constructor of our child class,
  // Adding a radius property
  // Adding the color which is already on our parent class,
  // that is why we don't have the public in front of color
  constructor(
    public radius: number,
    color: string,
  ) {
    super(color)
    // Instentiate the constructor of the parent with the color property.
  } // Now we have to implement this method cause it is abstract
 
  override render(): void {
    console.log("Rendering a circle")
  }
}
 
let circle = new Circle(666, "red")
console.log(circle)

Another example

abstract class Animal {
  abstract makeSound(): void // Abstract method
 
  move(): void {
    console.log("Animal is moving")
  }
}
 
class Dog extends Animal {
  makeSound(): void {
    console.log("Dog barks")
  }
}
 
class Cat extends Animal {
  makeSound(): void {
    console.log("Cat meows")
  }
}
 
const dog = new Dog()
const cat = new Cat()
 
dog.makeSound() // Output: Dog barks
dog.move() // Output: Animal is moving
 
cat.makeSound() // Output: Cat meows
cat.move() // Output: Animal is moving

In this example, Animal is an abstract class with an abstract method makeSound(). The Dog and Cat classes extend Animal and provide concrete implementations for the makeSound() method.

Generics

Generics in TypeScript allow you to define placeholders for types that are filled in when a function, class, or interface is used. This enables you to create components that work with various types.

What problem are they trying to solve ?

The problem they are trying to solve

class keyValuePair {
  constructor(
    public key: number,
    public value: string,
  ) {}
}
let pair = new KeyvaluePair(1, "Apple")
// let pair=new KeyvaluePair('1','Apple')

We are happy above we have our key value pair well set!

Now the big problem is when you want to use your key as a string !what about that?

With our current implementation we have got two solutions

The first solution is to use any when declaring our type,

This will surely work, but as we already know we don’t really want to use the any type at least the least amount of time possible. And by using any, we simply loose our intellisence Type Safety&checkecking and now typescript is back to being booring.

class keyPair {
  constructor(
    public key: any,
    public value: string,
  ) {}
}
let pair1 = new keyPair(1, "Apple")
let pair2 = new keyPair("a", "Apple")

The other solution is to duplicate our class.

class KeyPair {
  constructor(
    public key: any,
    public value: string,
  ) {}
}
class StringKeyPair {
  constructor(
    public key: any,
    public value: string,
  ) {}
}
let pair1 = new KeyPair(1, "Apple")
let pair2 = new StringKeyPair("a", "Apple")

This one simply defies the concept of DRY. and is very redundant, we don’t want to repeat ourselves. and if I want the key to be a Boolean? I will have to

We need to come up with a better way at least to easy the work for ourselves and that is what generics come to solve.

Using generic classes

class keyPair<T> {
  constructor(
    public key: T,
    public value: string,
  ) {}
}
 
let pair1 = new keyPair<number>(1, "Apple")
let pair2 = new keyPair<string>("a", "Apple")

Multiple generic types

class keyPair<K, V> {
  constructor(
    public key: V,
    public value: V,
  ) {}
}
let pair1 = new keyPair<number, string>(1, "Apple")
let pair2 = new keyPair<string, string>("a", "Apple")

If we don't supply the key value on our constructors, the compiler will infer when compiling.

let pair1 = new keyPair<number, string>(1, "Apple")
// This is perfectly normal.

Generic function

Generic Functions: You can define functions that work with multiple types using generics

function identity<T>(value: T): T {
  return value
}
const numberValue = identity(42) // number
const stringValue = identity("hello") // string

Example2:

function wrapInArray(value: number) {
  return [value]
}
let num = wrapInArray(23) //😉works
let str = wrapInArray("23") // Does not work , cause our argument is of type number

Generic Class method

You can also have a generic function inside of a class and that would just be alright

class ArrayUtils {
  static wrapInArray<T>(value: T) {
    return [value]
  }
}
 
let numbers = ArrayUtils.wrapInArray(1)
 
console.log(numbers)

Generic Interface

We can also make our interfaces generic!. Interfaces can be defined with generic parameters to work with various types.

interface KeyValuePair<K, V> {
  key: K
  value: V
}
 
const pair: KeyValuePair<string, number> = { key: "age", value: 25 }

Constraints on Generics:

Generics in TypeScript allow you to create components that can work with various types. However, sometimes you want to narrow down the set of types that can be used with a generic. This is where constraints come in. Generic constraints restrict the types that can be used with a generic parameter, ensuring that the types used meet certain criteria.

The Structure of Generic Constraints:

In TypeScript, you can apply constraints to a generic parameter by using the extends keyword followed by a type. This type serves as the constraint, specifying the allowed types for the generic parameter.

function genFunc<T extends ConstraintType>(param: T): void {
  // Code using 'param'
}

Why Use Constraints:

Constraints are valuable for multiple reasons:

  • 💡 Type Safety:
    • ➡️ Constraints ensure that the generic function or class operates only on types that satisfy specific criteria, reducing the risk of runtime errors.
  • 💡 Method Availability:
    • ➡️ Constraints can be used to ensure that specific methods or properties are available on the types used with the generic.

How many constraints can you

Constraint on union type

We have constrained our types to number and string

function echo<T extends number | string>(value: T): T {
  console.log(value)
  return value
}
echo("Funny") //You can do this
echo(66) // And this
echo({ good: "morning", yes: "Man" }) // Now this won't work!
function firstElement<T extends Array<any> | string>(input: T): T[0] {
  return input[0]
}
 
console.log(firstElement([1, 2, 3])) // Output: 1
console.log(firstElement("hello")) // Output: "h"

Constraint On interfaces

interface Person {
  name: string
}
 
function hello<T extends Person>(value: T): T {
  return value
}
 
console.log(hello({ name: "daniel" }))

Constraint on object shape

function printLength<T extends { length: number }>(input: T): void {
  console.log(input.length)
}
 
printLength("hello") // 5
printLength([1, 2, 3]) // 3
printLength({ length: 10 }) // 10

Constraints for method availability

interface Printable {
  print(): void
}
 
function printItem<T extends Printable>(item: T): void {
  item.print()
}
 
class Book implements Printable {
  print(): void {
    console.log("Printing book...")
  }
}
 
class Magazine {
  print(): void {
    console.log("Printing magazine...")
  }
}
 
const book = new Book()
const magazine = new Magazine()
 
printItem(book) // Output: Printing book...
printItem(magazine) // Output: Printing magazine...

Advanced Constraints:

You can create more complex constraints by combining types using intersections, unions, or other type operations.

Typescript Tips and tricks

@ts-expect-error and @ts-ignore Directives @ts-nocheck

@ts-expect-error and @ts-ignore are TypeScript directive comments that are used to suppress TypeScript compiler errors or warnings in specific code blocks. While they can be helpful in certain situations, it’s important to use them judiciously, as they can potentially mask real issues in your code.

1. @ts-expect-error

The @ts-expect-error comment is used to signal to the TypeScript compiler that an error is expected to occur at a certain location in your code. This can be useful when you know that a particular line of code should trigger an error, but you still want the rest of your code to be type-checked correctly.

// @ts-expect-error
const numberValue: number = "string" // This line intentionally triggers a type error

By using @ts-expect-error, you indicate to TypeScript that this error is intentional and should not be treated as a problem.

@ts-ignore

The @ts-ignore comment is used to instruct the TypeScript compiler to ignore the error or warning that would normally be generated for the following line of code.

// @ts-ignore
const result: number = someFunctionThatDoesNotExist() // No error will be reported for this line

While @ts-ignore can be helpful in quickly suppressing errors that you know are not problematic, it's also risky because it can hide legitimate issues in your code.

@ts-nocheck

The @ts-nocheck comment is used to disable TypeScript checking in JavaScript files. It instructs the TypeScript compiler to skip type checking for the entire JavaScript file.

// @ts-nocheck
const value = "hello"
console.log(value.toFixed(2)) // TypeScript will not check this line for type correctness

@ts-expect-error-type

The @ts-expect-error-type comment is used to expect a specific type error in TypeScript. This is similar to @ts-expect-error, but it allows you to specify the expected error message.

// @ts-expect-error-type Argument of type 'string' is not assignable to parameter of type 'number'.
const numberValue: number = "string" // TypeScript will expect the specified error

@ts-nocheck

The @ts-nocheck comment is used to disable TypeScript checking for a specific line of code. It’s similar to @ts-ignore, but it only applies to the next line.

const result: number = someFunctionThatDoesNotExist()
// @ts-nocheck
console.log(result.toFixed(2)) // TypeScript will not check this line for type correctness

While these directive comments can provide flexibility and control over the TypeScript compiler's behavior, it's important to use them thoughtfully and document their purpose clearly to ensure maintainability and code quality.

Typescript utility types

TypeScript utilities are built-in helper types that extend the language’s functionality. They allow you to manipulate and transform types in various ways, making your code more robust and readable. TypeScript utilities come as part of the standard library and are incredibly useful for enhancing your codebase.

Omit utility

Omit utility allows you to copies all properties from the original type except the ones we choose to exclude.

The Omit utility is especially handy when you need to create new types by removing certain properties

type User = {
  name: string
  age: number
  location: string
}
type MyUser = Omit<User, "name">
type Person = {
  name: string
  age: number
  email: string
}
 
type PersonWithoutEmail = Omit<Person, "email">

Pick

The Pick utility allows you to select a subset of properties from an existing type. Here’s how you can use it:

type Person = {
  name: string
  age: number
  email: string
}
 
type PersonNameAndAge = Pick<Person, "name" | "age">

Partial

The Partial utility is used to create a type where all properties of an existing type become optional. It’s beneficial when working with optional properties, such as form inputs or configuration objects:

type Configuration = {
  host: string
  port: number
}
 
type PartialConfiguration = Partial<Configuration>

Record

The Record utility creates an object type with specified keys and a shared value type:

type Weekdays = "Monday" | "Tuesday" | "Wednesday" | "Thursday" | "Friday"
type DaysMap = Record<Weekdays, string>

Exclude and Extract

Exclude and Extract are two utilities that operate on union types. Exclude removes values from a union type, and Extract selects values from a union type:

type Color = "Red" | "Green" | "Blue" | "Yellow"
 
type PrimaryColors = Extract<Color, "Red" | "Blue"> // 'Red' | 'Blue'
type NonGreenColors = Exclude<Color, "Green"> // 'Red' | 'Blue' | 'Yellow'

Required

The Required utility is the opposite of Partial. It makes all properties in a type required:

type OptionalPerson = {
  name?: string
  age?: number
}
 
type RequiredPerson = Required<OptionalPerson>

TypeScript Top Bottom Case

In typescript you can double case to a top type such as any unknown down to the type of your choice using double types

For you my children who wants simply to suicide yourself, i have got a perfect remedy for you! the double type

What if you are crazy enough to want to use ts but still want to act smart about it and use types that are not supposed to work ?

let date = "Hello" as Date
// the above will not work obviously, cause dates and strings can not be overlapped in any way !
// But there is a way you can work around this
 
let date = "Hello" as any as Date
// And voila, a precise way of killing yourself indeed !
 
date.toISOString()