🤖 TypeScript: Generics 🚀
Tags: typescript, javascript, beginners
Published: 2023-04-05 00:30:00


Generics in TypeScript allow you to write code that is not tied to a specific data type. Instead, you can create a generic type that can be used with any data type, making your code more efficient and easier to maintain. TypeScript generics are similar to C# and Java generics, and they provide a way to create reusable code that is both type-safe and flexible.


- 🚗 **Vehicle fleet management**

A vehicle fleet management system can use TypeScript generics to work with any type of vehicle
```typescript
class Vehicle<T> {
  make: string;
  model: string;
  year: number;
  data: T;

  constructor(make: string, model: string, year: number, data: T) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.data = data;
  }
}
```

- 🎲 **Game dice**

A dice in a game can have multiple sides, and we can use TypeScript generics to represent the possible values of each side and the number of sides

```typescript
class Dice<Sides extends number, Value> {
  sides: Sides;

  constructor(sides: Sides) {
   this.sides = sides;
  }

  roll(): Value {
   return Math.floor(Math.random() * this.sides) as Value;
  }
}

const dice = new Dice<6, string>(6);
console.log(dice.roll());
// returns a random string value between 0 and 5
```
- 🧬 **DNA sequence**

A DNA sequence can be represented as a string of nucleotides (A, C, G, T). We can use TypeScript generics to create a DNA class that can work with any sequence of nucleotides and a name
```typescript
type Nucleotide = "A" | "C" | "G" | "T";

class DNA<Sequence extends Nucleotide[], Name extends string> {
  sequence: Sequence;
  name: Name;

  constructor(sequence: Sequence, name: Name) {
    this.sequence = sequence;
    this.name = name;
  }

  toString(): string {
    return `${this.name}: ${this.sequence.join("")}`;
  }
}

const dna = new DNA(['A', 'C', 'G', 'T'], 'DNA Sequence');
console.log(dna.toString()); // returns "DNA Sequence: ACGT"

```

- 🍝 **Recipe**

Let's say we want to create a recipe class that can hold any type of ingredient (meat, vegetables, fruits, etc.). We can use generics to create a recipe class that can work with any type of ingredient:

```typescript
class Recipe<Ingredient> {
  ingredients: Ingredient[];

  constructor(ingredients: Ingredient[] = []) {
    this.ingredients = ingredients;
  }

  add(ingredient: Ingredient) {
    this.ingredients.push(ingredient);
  }

  remove(ingredient: Ingredient) {
    const index = this.ingredients.indexOf(ingredient);
    if (index !== -1) {
      this.ingredients.splice(index, 1);
    }
  }
}
```
This allows us to create a recipe that can hold any type of ingredient:

```typescript
interface Meat {
  name: string;
  cut: string;
}

interface Vegetable {
  name: string;
  color: string;
}

const recipe = new Recipe<Meat | Vegetable>([
  { name: 'Beef', cut: 'Chuck' },
  { name: 'Carrots', color: 'Orange' },
]);

recipe.add({ name: 'Chicken', cut: 'Breast' });
recipe.add({ name: 'Spinach', color: 'Green' });

console.log(recipe.ingredients); 
// returns an array of meats and vegetables
```

- 📝 **Form validation**

Let's say we want to create a form validation function that can handle multiple types of form fields (text fields, checkboxes, radio buttons, etc.)

```typescript
type FormField<T> = {
  value: T;
  required: boolean;
  validator?: (value: T) => boolean;
};

function validateForm<T>(fields: FormField<T>[]): boolean {
  return fields.every(field => {
    if (field.required && !field.value) {
      return false;
    }
    if (field.validator && !field.validator(field.value)) {
      return false;
    }
    return true;
  });
}

const textFields: FormField<string>[] = [
  { value: 'John Doe', required: true },
  { value: 'johndoe@example.com', required: true, validator: value => value.includes('@') },
];
const checkboxFields: FormField<boolean>[] = [
  { value: true, required: true },
  { value: false, required: true },
];

console.log(validateForm(textFields)); // returns true
console.log(validateForm(checkboxFields)); // returns false
```

- 🔍 **Object key lookup**

Let's say we want to create a function that can fetch a value from an object using multiple keys of different types. We can use generics to create a function that can work with multiple type arguments to fetch the value from the object

```typescript
type FetchValue<T, K extends readonly string[]> =
  K extends [infer First, ...infer Rest]
    ? First extends keyof T
      ? Rest extends []
        ? T[First]
        : FetchValue<T[First], Rest>
      : never
    : never;


type User = {
  id: number;
  name: string;
  address: {
    street: string;
    city: string;
  };
};

const user: User = {
  id: 1,
  name: "John",
  address: {
    street: "123 Main St",
    city: "Anytown"
  }
};

// Fetch the type of user.name
type UserName = FetchValue<User, ["name"]>; // string

// Fetch the type of user.address
type UserCity = FetchValue<User, ["address"]>;  // { street: string, city: string } 


// Fetch the type of user.address.city
type UserCity = FetchValue<User, ["address", "city"]>; // string
```