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
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:
// 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.
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.
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:
{
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:
{
key: 'age',
value: 32
}
The following class uses the KeyValuePair
interface:
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:
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
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.
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;
}