The Practical Client

Exploiting Generics in TypeScript

Generic classes and functions are one way to create flexible, reusable classes and functions. But before you start creating your own generic functions, you should be clear on when they’re your best choice.

Using generics in TypeScript is very similar to using them in, for example, C#. This method calls a generic function, specifying the data type to be used (string, in this case):

res = MyGenericFunction<string>("Peter");

And, as in other Microsoft .NET Framework languages, you can often count on the compiler to figure out the data type to be used with the function. If MyGenericFunction uses the data type for its first parameter, then, by passing a string in that parameter, I've told TypeScript that I want the data type to be a string (this is called "type inference"). That means I can reduce my call to MyGenericFunction to this:

res = MyGenericFunction("Peter");

You can even declare variables to hold generic functions and then store a generic function in that variable. The syntax for declaring a variable to hold a generic function is:

  • The var keyword
  • The variable name followed by a colon
  • The data type marker enclosed in angle brackets (conventionally, this is the letter T)
  • Parameter specifications enclosed in parentheses (these can use the data type marker)
  • The fat arrow (=>)
  • The return type (this can, again, use the data type marker)

The following example defines MyGenericVariable as being able to hold a function that accepts and returns the same type, both specified using the data type marker. Just to demonstrate that you don't always have to use an uppercase T as the data type marker, I've let my freak flag fly and used a lowercase t in this code:

var genericVariable: <t>(parm1: t) => t;
genericVariable = MyGenericFunction;
resString = genericVariable("Peter");

In addition to generic functions, TypeScript also includes generic classes. This code specifies the string type is to be used with a class called GenericClass:

var gc: MyGenericClass<string>;
gc = new MyGenericClass<string>();
var res: number;
res = gc.GenericMethod("Peter");

And that's fine … as long as all you want to do is call generic functions and generic classes. If, however, you want to create your own generic functions/classes, then there's more to talk about.

When to Use Generic Functions
The primary reason you create a generic function is because you have some code that meets two criteria:

  • It's a function or class that will work with a variety of data types
  • The function or class uses that data type in several places

You have other options: When you have code that works with a variety of data types, you could write one version of the function for each data type, but that's a lot of work and a maintenance nightmare. You could also write one function and have it use the any keyword for its data types, but that would mean abandoning type safety.

Provided that the data types involved are used in several places in your code, a generic function or class is a better solution than your other options: generic functions let you write one version of your code and ensure that the code consistently uses data types (and then, of course, let the developer specify the data type when calling the function/instantiating the class).

Defining a Simple Generic Function
To create a generic function in TypeScript, you just add angle brackets after the function's name to enclose one or more data type markers. You can then use those data type markers in your function instead of actual data types. This example defines a function with two type markers and then uses them to declare internal variables and specify the function's return type:

function MyOtherGenericFunction<q,r>(parm1: q): r 
{
  var variable1: q;
  var variable2: r;
  return variable2;
}

Declaring a generic class is just as simple: Just follow the class name with type markers enclosed in angle brackets. The following code declares a class with the traditional T marker and then uses that to define a field, the input parameters of a method, the output type of a method and a variable inside that method:

class MyGenericClass<T> 
{
  private field1: T;
  GenericMethod(name: T): T 
  {
    var variable1: T;
    return variable1;
  }
}

While these examples will compile, the resulting code will be almost useless to you in TypeScript. With these definitions, TypeScript has to assume that literally any data type could be used with this code. As a result, TypeScript won't let you perform any operation with those variables unless the operation is valid for any data type. That doesn't leave much you can do.

Creating Useful Generic Functions: Constraints
However, this isn't as limiting as you might expect because you don't typically expect your function to work with any data type: You write generic functions to work with a range of data types, rather than with every possible data type. If you tell TypeScript what your range of data types is ("constrain your data type"), then TypeScript will let you use any functionality that's common to those data types.

You implement constraints on data types using the TypeScript extends keyword. The extends keyword, however, only works with interfaces and classes, so "extends number" or "extends string" won't compile, even if you wanted to use them.

Your best choice for a constraint is an interface because you can apply interfaces to any arbitrary collection of classes. Here's an interface, for example, that defines two properties, one of type string and one of type number:

interface ICustomer
{
  name: string;
  age: number;
}

With that interface in place, I can now write a generic function that constrains the data type to accept any class that implements my ICustomer interface. Because the data type is constrained to "things that implement the ICustomer interface," I can use the members of the ICustomer interface within my function. This generic function, for example, uses the ICustomer's age property (and, again, I'm using something other than the traditional T for the data type marker):

function CalcAverageAge<c extends ICustomer>(cust1: c, cust2: c): number
{
  return (cust1.age + cust2.age)/2;
}

I've now got a flexible function that's still type safe. I can call this method with any class with a compatible interface or even with compatible object literals, as I do here:

resNumber = CalcAverageCustomerAge({name: "Peter", age: 62},
                                   {name: "Jason", age: 33});

I can even rewrite this to a more useful version by having the function accept an array of "anything that implements the ICustomer interface":

function CalcAverageAge<q extends ICustomer>(custs: q[]): number
{
    var totAge: number;
    custs.forEach(function(c) {totAge += c.age});
    return totAge/custs.length;
}

However, this isn't a great example of a generic function. I could have achieved the same result by simply declaring my parameter as ICustomer:

function CalcAverageAge(custs: ICustomer[]): number

And this brings me back to my second criteria for using generics: The data type marker is needed in several places in your code. Using generics ensures that a data type is used consistently throughout a generic function or class. In my example, I'm using my ICustomer interface precisely once, in the function's parameter list. It doesn't seem worthwhile to me to define the function as a generic.

However, if I use the ICustomer data type in multiple places in my function, creating that function as a generic pays off by ensuring that the datatype is used consistently throughout the function. Listing 1 shows a more useful example of a generic function: This function not only calculates the average age, but also finds a customer whose age matches that average age. In this function, it's important that the input parameter (an array), the output type (a singleton) and two internal variables are all of the specified type. Using a generic function ensures that I use the same type throughout.

Listing 1: A Useful Generic Function
function FindAverageCustomerByAge<C extends ICustomer(custs: C[]): C 
{
  var totAge: number;
  totAge = 0;
  custs.forEach(function(cust) {totAge += cust.age});

  var averageAge: number;
  averageAge = totAge/custs.length;

  var averageCusts: C[];
  averageCusts = custs.filter(c => c.age == averageAge);

  var averageCust: C;
  averageCust = averageCusts[0];

  return averageCust;
}

This is why generic classes are, in my opinion, even more useful than generic functions: A generic class ensures that the specified data type is used consistently throughout a whole class. Besides, when used throughout a class, generic type markers save you more typing than is possible in a single function. Even if you’re paid by the hour (as I am), this is a good thing: The less typing I do, the fewer mistakes I make.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube