# TypeScript Advanced Type Techniques Generics, Union, and Intersection Types

## Generics

In TypeScript, generics are a powerful tool that allow us to write reusable components capable of adapting to multiple types.

### 1\. Generic Constraints

Generics can be constrained to a specific type or interface, ensuring that the types passed to the generic meet certain conditions. For example, if we want a function to only accept types with a `length` property, we can do this:

```typescript
interface Lengthwise {
  length: number;
}

function printLength<T extends Lengthwise>(item: T): void {
  console.log(item.length);
}

const stringLength = "hello";
printLength(stringLength); // OK, strings have a length property
const objectWithoutLength = { name: "World" };
printLength(objectWithoutLength); // Error, no length property
```

`<T extends Lengthwise>` ensures that `T` must have a `length` property.

### 2\. Type Inference with Generics

TypeScript allows automatic inference of generic types in certain cases, particularly during function calls. For example, using the `infer` keyword, we can extract the return type from a function type:

```typescript
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function identity<T>(arg: T): T {
  return arg;
}

type IdentityReturnType = ReturnType<typeof identity>; // IdentityReturnType is inferred as 'string'
```

Here, `ReturnType` extracts the return type from the function type.

### 3\. Multi-Parameter Generics

You can define types or functions that accept multiple generic parameters:

```typescript
interface Pair<T, U> {
  first: T;
  second: U;
}

function createPair<T, U>(first: T, second: U): Pair<T, U> {
  return { first, second };
}

const pair = createPair("Hello", 42); // pair has type Pair<string, number>
```

The `createPair` function accepts two generic parameters `T` and `U` and returns an object of type `Pair<T, U>`.

### 4\. Generic Interfaces

Interfaces can also use generics:

```typescript
interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity; // myIdentity is a number-specific version of the identity function
```

Here, `GenericIdentityFn` is a generic interface, and the `identity` function is assigned to an instance of it, restricting it to handle only `number` type parameters.

### 5\. Generic Classes

Classes can also be defined as generics, allowing methods and properties to use different types:

```typescript
class Box<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  setValue(newValue: T): void {
    this.value = newValue;
  }
}

const boxOfStrings = new Box<string>("Hello");
boxOfStrings.setValue("World"); // OK
boxOfStrings.setValue(123); // Error, incompatible types
```

The `Box` class accepts a generic type `T`, and the specific type is specified during instantiation.

## Union Types

Union types in TypeScript allow combining multiple types into a single type, meaning a variable can be one of several types.

### 1\. Type Guards and Type Assertions

When dealing with union types, you may need to determine the specific type of a variable. This can be achieved with type guards, which are functions or expressions that check a variable's properties to narrow its possible type range. For example:

```typescript
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function getArea(shape: Shape): number {
  if ('radius' in shape) {
    // Type guard: shape is now { kind: 'circle'; radius: number }
    return Math.PI * shape.radius ** 2;
  } else {
    // Type guard: shape is now { kind: 'square'; side: number }
    return shape.side ** 2;
  }
}
```

In this example, checking if `shape` has a `radius` property determines whether it’s a circle or a square.

### 2\. Non-null Assertion Operator (!)

The non-null assertion operator `!` informs the compiler that, even if a union type includes `null` or `undefined`, you are certain the value is neither. However, incorrect usage may lead to runtime errors:

```typescript
function logValue(value: string | null | undefined): void {
  if (value) {
    console.log(value!.toUpperCase()); // Use ! for non-null assertion
  } else {
    console.log('Value is null or undefined');
  }
}
```

Here, if `value` is not `null` or `undefined`, we use `!` to suppress potential `null` or `undefined` type warnings.

### 3\. Distributive Type Operator (& and |)

When applying union or intersection types to a generic type, they are distributed across each instance of the generic. For example, with an array whose elements are a union type:

```typescript
type NumbersOrStrings = number | string;
type ArrayWithMixedElements<T> = T[];

const mixedArray: ArrayWithMixedElements<NumbersOrStrings> = [1, "two", 3];
```

`mixedArray`’s element type is `NumbersOrStrings`, so it can contain `number` or `string`.

### 4\. Pattern Matching

In destructuring assignments, function parameters, or type aliases, pattern matching can be used to handle values of union types:

```typescript
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function handleShape(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      const { radius } = shape;
      // Now we know shape is { kind: 'circle'; radius: number }
      break;
    case 'square':
      const { side } = shape;
      // Now we know shape is { kind: 'square'; side: number }
      break;
  }
}
```

In this example, the `switch` statement acts as a type guard, handling different shape types based on the `kind` property.

## Intersection Types

Intersection types in TypeScript allow combining multiple types into a new type that includes all properties and methods of the original types.

### 1\. Combining Types

Intersection types use the `&` operator to merge two or more types. For example, suppose we have two interfaces, `Person` and `Employee`. We can create a `PersonAndEmployee` intersection type:

```typescript
interface Person {
  name: string;
  age: number;
}

interface Employee {
  id: number;
  department: string;
}

type PersonAndEmployee = Person & Employee;

const person: PersonAndEmployee = {
  name: 'Alice',
  age: 30,
  id: 123,
  department: 'HR',
};
```

The `person` variable must satisfy the requirements of both `Person` and `Employee` interfaces.

### 2\. Intersection of Classes and Interfaces

Intersection types can also be applied between classes and interfaces, combining a class instance with the properties and methods of an interface:

```typescript
class Animal {
  name: string;
  makeSound(): void {
    console.log('Making sound...');
  }
}

interface HasColor {
  color: string;
}

class ColoredAnimal extends Animal implements HasColor {
  color: string;
}

type ColoredAnimalIntersection = Animal & HasColor;

function describeAnimal(animal: ColoredAnimalIntersection) {
  console.log(`The ${animal.name} is ${animal.color} and makes a sound.`);
  animal.makeSound();
}

const coloredCat = new ColoredAnimal();
coloredCat.name = 'Kitty';
coloredCat.color = 'Gray';
describeAnimal(coloredCat);
```

The `ColoredAnimalIntersection` type is both an instance of the `Animal` class and possesses the `color` property from the `HasColor` interface.

### 3\. Type Guards

Intersection types are useful in type guards, especially when determining a specific type within a union type. For example, you might have an object that could be one of two types, and you want to determine which it is at a specific moment:

```typescript
interface Movable {
  move(): void;
}

interface Static {
  stay(): void;
}

type ObjectState = Movable & Static;

function isMovable(obj: ObjectState): obj is Movable {
  return typeof obj.move === 'function';
}

const object: ObjectState = {
  move: () => console.log('Moving...'),
  stay: () => console.log('Staying...')
};

if (isMovable(object)) {
  object.move(); // Type guard ensures the move method exists
} else {
  object.stay();
}
```

The `isMovable` function is a type guard that checks if the `move` method exists, confirming that `object` is of type `Movable`.
