Further examples of generics in TypeScript

Basic function

In the code below we have a simple JavaScript function that receives a value and an an array as parameters. It returns a new array comprising the original array plus the additional value:

function generateArray(existingArray, newValue) {
  return [...existingArray, newValue];
}

Imagine that we want to ensure that each of the parameters share the same data type. In other words: if the function is passed a string array, the second parameter must also be a string. For example, it should not be the case that you can append a string to an array of numbers.

Now imagine that we don’t know in advance what type the value or array will be, we just know that the data types of the parameters must match.

In converting the function to TypeScript, one way of overcoming our lack of foreknowledge would be to deploy any. This way it doesn’t matter which types are passed to the function:

function generateArray(existingArray: any[], newValue: any): any[] {
  return [...existingArray, newValue];
}

But this is no solution at all. The problem — as always with any — is that it strips our function of any type checks whatsoever and would therefore invite calls of form: generateArray([1,2,3], 'lorem').

Enter generics:

function generateArray<T>(existingArr: T[], newValue: T): T[] {
  return [...existingArr, newValue];
}

Now, whilst we haven’t asserted ahead of time which types will be used, whichever types we do pass in, must match. The function header is saying:

  • both arguments must be of the same type (represented by T)
  • the function will return an array of this same T type.

If I then tried to run the function with unmatched types (for example generateArray([1,2,3,4], true) ) TypeScript would raise the following error:

Argument of type 'boolean' is not assignable to parameter of type 'number'

Note that even though the function in question does not express any preference for number types, given that our first parameter is a number, TypeScript knows that the second parameter must also be a number.

In the generic function we have used T as our placeholder for a generic type as this is the convention. However there is no compunction to do so. We could have used any letter or string, providing that the string is not a reserved term.

More advanced function

This example demonstrates how we can use generics to reduce repetition when writing functions and is also a more realistic use case.

Let’s say we have two types or interfaces:

type VideoFormatUrls = {
  format720p: URL;
  format1080p: URL;
};
type SubtitleFormatUrls = {
  english: URL;
  german: URL;
};

An example of an object matching these type definitions:

const videoFormats: VideoFormatUrls = {
	format720p: https://www.format720p.co.uk,
	format1080p: https://www.format1080p.co.uk
}

Imagine we wanted to be able to check whether a given film is available in a certain video format. We could write a function like this:

function isFormatAvailable(
  obj: VideoFormatUrls,
  format: string
): format is keyof VideoFormatUrls {
  return format in obj;
}

Now imagine that we need to do the same thing with subtitles, but given that isFormatAvailable() is typed to the VideoFormatUrls type we would get an error if we used this function for subtitles. But we also don’t want to write a near identical function typed to SubtitleFormatUrls to subtitles just to ensure adequate type safety.

Alternatively we could use a union type, for example:

function isFormatAvailable(
  obj: VideoFormatUrls | SubtitleFormatUrls,
  format: string
): format is keyof VideoFormatUrls {
  return format in obj;
}

But this quickly becomes unwieldy if we, for the sake of argument have a great many URL types that we want the function to utilise.

This is where generics become super helpful. Here is how we would rewrite the function as a generic:

function isAvailable<Formats>(obj: Formats, key: string): key is keyof Formats {
  return key in obj;
}

We could then explicitly type our calls of this function, viz:

isFormatAvailable<SubtitleFormatUrls>(subtitles, "english");