---
tags:
  - typescript
---

# Generics

Generics are a powerful feature in TypeScript that enables you to write reusable
and flexible code while maintaining strong typing. With generics, you can create
functions, classes, and interfaces that work with various types while preserving
type information.

The main purpose of generics is to allow developers to write code that can
**operate on different data types without knowing the specific type
beforehand**. This helps to keep the code DRY and maintainable.

## Basic usage

### Functions

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

Here, `T` is a generic type variable. The `identity` function is a generic
function that takes an argument of type `T` and returns a value of type `T`.

To use this generic function you can either explicitly provide the type within
the angle brackets, or let TypeScript infer the type based on the value passed:

```ts
// Explicitly specifying the type
let output1 = identity<string>("hello");

// TypeScript infers the type as `number`
let output2 = identity(42);
```

#### Restricting the available types

In the previous example any type could be used with the `identity` function. The
only constraint that we place on usage is that the types must be consistent: if
we pass a string as an argument, then a string must be returned.

However we can add further restrictions on types by using the `extend` keyword,
combined with an interface.

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

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}
```

In this example the `logLength` generic function is limited to types that
implement the `Lengthwise` interface. So: any argument that is passed to
`logLength` and any value that is returned by it must match the shape of
`Lengthwise`, having the `length` property.

## Interfaces and classes

Generics can also be used profitably when working with stricter OOP constructs.

```ts
interface KeyValuePair<K, V> {
  key: K;
  value: V;
}
```

The `KeyValuePair` interface has two generic type parameters: K for the key and
V for the value. The interface defines an object shape that must possess the
properties `key` and `value`, e.g:

```ts
{
  key: 2,
  value: 'something'
}
```

In this form the generic specifies that whatever types are used for the key and
the pair must be used consistently, e.g. this would be wrong:

```ts
{
  key: 'age',
  value: 32
}
```

The following class uses the `KeyValuePair` interface:

```ts
class Storage<K, V> {
  private items: KeyValuePair<K, V>[] = [];

  add(item: KeyValuePair<K, V>): void {
    this.items.push(item);
  }

  getByKey(key: K): V | undefined {
    const foundItem = this.items.find((item) => item.key === key);
    return foundItem ? foundItem.value : undefined;
  }

  getAll(): KeyValuePair<K, V>[] {
    return this.items;
  }
}
```

This class stores objects that match the `KeyValuesPair` interface in an array
and provides and add and list method for accessing/returning them.

The `add` method takes an item of type `KeyValuePair<K, V>` and adds it to the
`items` array. The `getByKey` method takes a key of type `K` and returns the
value of type `V` associated with that key or undefined if the key is not found.
The `getAll` method returns all stored key-value pairs.

Here is an example of instantiating the class:

```ts
const storage = new Storage<number, string>();

storage.add({ key: 1, value: "one" });
storage.add({ key: 2, value: "two" });

const value = storage.getByKey(1);
const allItems = storage.getAll();

console.log(value);
// value: "one"

console.log(allItems);

// allItems: [
//   { key: 1, value: "one" },
//   { key: 2, value: "two" }
// ]
```

## Real examples

### GraphQL client for query and mutation requests over `fetch`

```ts
type GraphQlResult<T> = {
  data: T;
  errors?: Array<{
    message: string;
    locations: Array<{ line: number; column: number }>;
    path: Array<string | number>;
  }>;
};

export class GraphQlClient {
  private endpoint: string;

  constructor(endpoint: string) {
    this.endpoint = endpoint;
  }
  async request<T>(
    query: string,
    variables?: Record<string, unknown>
  ): Promise<T> {
    try {
      const response = await fetch(this.endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ query, variables }),
      });

      if (!response.ok) {
        throw new Error(
          `Network error: ${response.status} - ${response.statusText}`
        );
      }

      const result: GraphQlResult<T> = await response.json();

      if (result.errors) {
        throw new Error(`GraphQL error: ${JSON.stringify(result.errors)}`);
      }

      return result.data;
    } catch (error) {
      console.error(error);
      throw error;
    }
  }
}
```

### VSCode extension TreeView generator

In VSCode a TreeView is a list of values that may have nested values, like a
directory. The following generic is a helper function that generates a TreeView
based on a given class that is passed in as an argument, along with the class's
constructor values (`args` in the example). It also calls a method `refresh` on
each instance of the class.

```ts
function createTreeView<
  T extends IndexHyperlinksProvider | IndexMetadataProvider,
  U extends LinkTypes | MetadataTypes
>(
  viewType: string,
  ProviderClass: new (...args: any[]) => T,
  type: U,
  activeEditor?: string | undefined,
  ...args: ConstructorParameters<typeof ProviderClass>
): T {
  const view = new ProviderClass(...args, type);
  if (view instanceof IndexHyperlinksProvider) {
    view.refresh(activeEditor, type as LinkTypes);
  } else if (view instanceof IndexMetadataProvider) {
    view.refreshIndex();
  }
  vscode.window.registerTreeDataProvider(viewType, view);
  return view;
}
```
