list all Type Annotations and explain in Typescript along with examples

  • string: Used to denote a string value. For example: const name: string = 'Alice';
  • number: Used to denote a numeric value. This includes both integers and floating-point values. For example: const age: number = 25;
  • boolean: Used to denote a boolean value (true or false). For example: const isActive: boolean = true;
  • void: Used to denote a value that has no type (e.g. a function that does not return a value). For example: function logMessage(message: string): void { console.log(message); }
  • null: Used to denote a null value. For example: const value: null = null;
  • undefined: Used to denote an undefined value. For example: const value: undefined = undefined;
  • any: Used to denote a value that can be of any type. For example: let value: any = 'hello'; value = 123;
  • object: Used to denote a value that is an object. For example: const obj: object = {};
  • Array: Used to denote an array of values. The type of the values in the array can be specified using a type parameter. For example: const names: string[] = ['Alice', 'Bob', 'Charlie']; Used to denote a fixed-length array with known element types. For example: const point: [number, number] = [0, 0]

Enum

Enum: Used to define a set of related values. For example:

enum Color {
  Red,
  Green,
  Blue
}

const color: Color = Color.Green;

Function:

Used to denote a function value. The types of the arguments and the return value can be specified using type parameters. For example

type AddFunction = (x: number, y: number) => number;

const add: AddFunction = (x, y) => x + y;

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

In this example, we have defined a type called AddFunction that represents a function that takes two numbers as arguments and returns a number. We have then defined a variable called add that has the type AddFunction, and assigned a function value to it.

We can then use the add function as we would any other function, by calling it with the required arguments. The function type annotation ensures that the function is used correctly and that the arguments and return value have the correct types.

Class:

class User {
  constructor(public name: string, public age: number) {}
}

const user: User = new User('Alice', 25);

//Another example - Example 2

class User {
  constructor(public name: string, public age: number) {}
}

type UserType = typeof User;

const UserClass: UserType = User;

const user = new UserClass('Alice', 25);

console.log(user.name);  // 'Alice'
console.log(user.age);  // 25

In second example, we have defined a class called User with a name and age property. We have then defined a type called UserType that represents the type of the User class. We have assigned the User class to a variable called UserClass, which has the type UserType.

We can then use the UserClass variable to create new instances of the User class, as shown in the example above. The class type annotation ensures that the class is used correctly and that the properties of the class have the correct types.

Type aliases:

Type aliases are used to create new names for existing types.

type StringOrNumber = string | number;

function concat(value1: StringOrNumber, value2: StringOrNumber): string {
  return String(value1) + String(value2);
}

const result1 = concat('hello', 'world');  // 'helloworld'
const result2 = concat(1, 2);  // '12'

In this example, we have defined a type alias called StringOrNumber that represents a value that can be either a string or a number. We have then used this type alias as a type parameter for the concat function, which concatenates two values and returns the result as a string.

We can then use the concat function with both string and number values, as shown in the examples above. The type alias allows us to use the same function with multiple different types of values, without having to define separate functions for each type.

Interfaces

interface User {
  name: string;
  age: number;
}

function greet(user: User) {
  console.log(`Hello, ${user.name}! You are ${user.age} years old.`);
}

const alice: User = { name: 'Alice', age: 25 };
greet(alice);  // 'Hello, Alice! You are 25 years old.'

In this example, we have defined an interface called User with a name and age property. We have then defined a function called greet that takes a user object as an argument. The argument is typed as the User interface, which means that it must have a name and age property with the correct types.

We have then created a variable called alice that is typed as the User interface, and passed it to the greet function. The interface ensures that the alice object has the correct structure and types, and the function is able to use the name and age properties as expected.

Union Types

type StringOrNumber = string | number;

function getLength(value: StringOrNumber): number {
  if (typeof value === 'string') {
    return value.length;
  } else {
    return value.toString().length;
  }
}

console.log(getLength('hello'));  // 5
console.log(getLength(12345));  // 5

In this example, we have defined a type called StringOrNumber that represents a value that can be either a string or a number. We have then defined a function called getLength that takes a value of this type as an argument and returns its length.

To handle the different types of values that the function might receive, we use a type check (typeof value === 'string') to determine the type of the value and perform the appropriate action.

We can then use the getLength function with both string and number values, as shown in the examples above. The union type allows us to use the same function with multiple different types of values, without having to define separate functions for each type.

Intersection Type

interface User {
  name: string;
  age: number;
}

interface Admin {
  canManageUsers: boolean;
}

type AdminUser = User & Admin;

const alice: AdminUser = { name: 'Alice', age: 25, canManageUsers: true };

console.log(alice.name);  // 'Alice'
console.log(alice.age);  // 25
console.log(alice.canManageUsers);  // true

In this example, we have defined two interfaces: User with a name and age property, and Admin with a canManageUsers property. We have then defined a type called AdminUser that represents an object that has the properties of both the User and Admin interfaces.

We have then created a variable called alice that is typed as the AdminUser type, and assigned an object that has the required properties. The intersection type ensures that the alice object has the correct structure and types, and we are able to access all of the properties as expected.

Type guards

function isString(value: any): value is string {
  return typeof value === 'string';
}

function getLength(value: string | number): number {
  if (isString(value)) {
    return value.length;
  } else {
    return value.toString().length;
  }
}

console.log(getLength('hello'));  // 5
console.log(getLength(12345));  // 5

In this example, we have defined a function called isString that takes a value of type any and returns a boolean indicating whether the value is a string. This function is called a type guard, because it allows us to narrow the type of the value within the if statement.

We have then defined a function called getLength that takes a value of type string or number and returns its length. To handle the different types of values that the function might receive, we use the isString type guard to determine the type of the value and perform the appropriate action.

We can then use the getLength function with both string and number values, as shown in the examples above. The type guard ensures that the value has the correct type and allows us to use the appropriate methods and properties for that type.

Type parameters:

function createArray<T>(length: number, value: T): Array<T> {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

const strings = createArray<string>(3, 'hello');  // ['hello', 'hello', 'hello']
const numbers = createArray<number>(3, 123);  // [123, 123, 123]

In this example, we have defined a function called createArray that takes a length and a value, and returns an array of that value. The function uses a type parameter, T, to represent the type of the value.

We can then call the createArray function with different types of values, as shown in the examples above. The type parameter allows us to use the same function with multiple different types of values, without having to define separate functions for each type.

The type parameter is inferred from the type of the value argument, so in the first call to createArray, the type parameter is inferred to be string, and in the second call, it is inferred to be number. This allows the function to return the correct type of array for each call.

Type assertions

let value: any = 'hello';

let strLength: number = (value as string).length;

console.log(strLength);  // 5

In this example, we have defined a variable called value with the type any, which means that it can hold any type of value. We have then assigned a string value to the variable.

We have defined another variable called strLength that is supposed to hold the length of the string value. To access the length property of the value variable, we use a type assertion to tell the TypeScript compiler that the value is a string.

The type assertion is written as value as string, and it tells the compiler to treat the value as a string, even though it has the any type. This allows us to access the length property of the string and assign it to the strLength variable.

We can then use the strLength variable as we would any other variable of type number. The type assertion ensures that the value has the correct type and allows us to use the appropriate methods and properties for that type.

How can i create a basic project structure and how can i decide which pattern i should use

When creating a new project, it can be helpful to start by defining the overall structure of your project and deciding on a suitable design pattern to use. There are many different design patterns that can be used in a variety of different contexts, so it is important to consider your project’s specific requirements and constraints when choosing a pattern.

Here are some steps you can follow to create a basic project structure and decide on a design pattern:

  1. Define the scope and goals of your project. What are you trying to achieve, and what are the key features and functionality that your project needs to have?
  2. Identify the main components of your project. Break down your project into smaller pieces that can be developed and tested separately.
  3. Determine the relationships between the different components of your project. How do the different pieces fit together, and how will they interact with each other?
  4. Research different design patterns that might be suitable for your project. Consider factors such as the complexity of your project, the need for flexibility and reuse, and the performance and scalability requirements of your project.
  5. Evaluate the pros and cons of each design pattern and consider how well it fits with your project’s requirements and constraints.
  6. Choose a design pattern that best meets the needs of your project, and plan out the overall structure of your project based on the chosen pattern.

It is important to keep in mind that design patterns are not a one-size-fits-all solution, and it is possible that your project may require a combination of different patterns to achieve the desired result. It is also important to be open to changing your design pattern as your project evolves and your understanding of the problem space deepens.

Quiz Game Example

interface Question {
  text: string;
  correctAnswer: string;
  possibleAnswers: string[];
}

class MultipleChoiceQuestion implements Question {
  constructor(
    public text: string,
    public correctAnswer: string,
    public possibleAnswers: string[]
  ) {}
}

class TrueFalseQuestion implements Question {
  constructor(
    public text: string,
    public correctAnswer: string,
    public possibleAnswers: string[] = ['True', 'False']
  ) {}
}

class QuestionFactory {
  static createQuestion(questionType: string, data: any): Question {
    switch (questionType) {
      case 'multiple-choice':
        return new MultipleChoiceQuestion(
          data.text,
          data.correctAnswer,
          data.possibleAnswers
        );
      case 'true-false':
        return new TrueFalseQuestion(data.text, data.correctAnswer);
      default:
        throw new Error(`Invalid question type: ${questionType}`);
    }
  }
}

const quizData = [
  {
    type: 'multiple-choice',
    text: 'What is the capital of France?',
    correctAnswer: 'Paris',
    possibleAnswers: ['Paris', 'London', 'Rome', 'Madrid']
  },
  {
    type: 'true-false',
    text: 'The Earth orbits the Sun.',
    correctAnswer: 'True'
  }
];

const quiz: Question[] = quizData.map((questionData) => {
  return QuestionFactory.createQuestion(questionData.type, questionData);
});

console.log(quiz);

In this example, the QuestionFactory class is used to create instances of Question objects based on the type of question being created. The createQuestion method takes a questionType parameter and a data object, and returns a new Question object of the appropriate type. The Question interface defines the common properties and methods that all Question objects should have, while the MultipleChoiceQuestion and TrueFalseQuestion classes provide specific implementations for different types of questions.

The quizData array contains data for two different questions, one of type 'multiple-choice' and one of type `’true-

false’. The mapfunction is used to iterate over thequizDataarray and create a newQuestionobject for each element using theQuestionFactory.createQuestionmethod. The resulting array ofQuestionobjects is then stored in thequiz` variable.

This example demonstrates how the Factory pattern can be used to create objects of different types based on input data, while maintaining a consistent interface for all objects and encapsulating the implementation details within the factory class. This can make it easier to maintain and extend the codebase, as new types of questions can be added simply by adding a new class and modifying the createQuestion method to handle the new type.

Online Store Example

Imagine that we are building an online store where customers can add items to their shopping cart and check out. The store sells three types of items: books, clothes, and electronics. Each type of item has its own set of specific properties (e.g. a book has an ISBN number and a page count, a piece of clothing has a size and a color, etc.), and we want to be able to add new types of items to the store in the future without having to change the existing code.

To solve this problem, we can use the Factory pattern to create a factory class that generates the appropriate objects based on the type of item. Here is how we might implement this using TypeScript:

interface Item {
  id: number;
  name: string;
  price: number;
  type: string;
}

interface Book extends Item {
  isbn: string;
  pageCount: number;
}

interface Clothes extends Item {
  size: string;
  color: string;
}

interface Electronics extends Item {
  manufacturer: string;
  model: string;
}

class ItemFactory {
  static createItem(type: string, data: any): Item {
    switch (type) {
      case 'book':
        return {
          id: data.id,
          name: data.name,
          price: data.price,
          type: 'book',
          isbn: data.isbn,
          pageCount: data.pageCount
        } as Book;
      case 'clothes':
        return {
          id: data.id,
          name: data.name,
          price: data.price,
          type: 'clothes',
          size: data.size,
          color: data.color
        } as Clothes;
      case 'electronics':
        return {
          id: data.id,
          name: data.name,
          price: data.price,
          type: 'electronics',
          manufacturer: data.manufacturer,
          model: data.model
        } as Electronics;
      default:
        throw new Error(`Invalid item type: ${type}`);
    }
  }
}

const book: Book = ItemFactory.createItem('book', {
  id: 1,
  name: 'The Great Gatsby',
  price: 15,
  isbn: '978-0