--- categories: - Programming Languages 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(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("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(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 { 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 { private items: KeyValuePair[] = []; add(item: KeyValuePair): 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[] { 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` 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(); 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 = { data: T; errors?: Array<{ message: string; locations: Array<{ line: number; column: number }>; path: Array; }>; }; export class GraphQlClient { private endpoint: string; constructor(endpoint: string) { this.endpoint = endpoint; } async request( query: string, variables?: Record ): Promise { 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 = 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 ): 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; } ```