Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Joseph Lamoree
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Joseph Lamoree ( @jlamoree )

Using Type Argument Inference When Accepting Generic Callbacks In TypeScript And Node.js

By on

The other day, when I was working on my Regular Expression Day puzzle finder, I ran into a TypeScript scenario that I didn't know how to solve. I had a class that had an internal collection and a public .map() method that accepted a callback. The callback could return any arbitrary Type; which means that the .map() method could return an Array of any arbitrary Type. In order to get this working, I parameterized the .map<T>() method when invoking it. But, I've since learned that I could have solved the problem more elegantly by using "type argument inference."

In my first attempt (when working on the RegEx Day puzzler), I had a method that accepted a callback that was parameterized to return an array of Type <T>:

public map<T>( callback: CallbackFunction ) : T[] { /* ... */ }

In this case, since the definition of <T> was arbitrary, I had to provide it as a Type parameter when invoking the .map() method (using <string> in this case):

var results = instance.map<string>( /* callback */ );

This worked well; but, I didn't like that I had to supply the <string> parameter, especially when I was passing in a strongly-typed Callback. I wanted TypeScript to be able to look at the Callback signature and automatically determine the return type of the .map() method.

It turns out, TypeScript can do this using a technique known as "type argument inference." This is a feature in which TypeScript looks at the Type of the method argument and uses that Type to automatically parameterize the method itself. In order for this to work, the method, the argument, and the return Type (if it has one) all need to be parameterized. This way, TypeScript understands how to pull the argument Type through to the rest of the method definition.

public map<T>( callback: CallbackFunction<T> ) : T[] { /* ... */ }

Now that the callback argument is parameterized - CallbackFunction<T> - TypeScript can use the argument Type in order to implicitly parameterize the method call using the same Type <T>. To see this in action, I've created a Tokens class that keeps an internal collection of tokens and exposes a .map() method that allows those tokens to be mapped using a callback:

// I define the callback function interface for the .map() method. This entire interface
// is parameterized with the given Type <T> so that we can facilitate "type argument
// inference" in the method signature for .map().
interface TokensMapCallback<T> {
	( token: string ) : T;
}

class Tokens {

	private _tokens: string[];

	// I initialize the tokens class with the given tokens.
	constructor( tokens: string[] ) {

		this._tokens = tokens;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I map the internal tokens using the given callback.
	// --
	// NOTE: By using <T> to parameterize both the method and the callback type, we
	// allow for "type argument inference", which is when TypeScript looks at the method
	// argument and uses its Type as way to automatically determine which type should be
	// used to parameterize the map<T> method. In this case, it will look at the return-
	// type of the callback as the parameterization type (as defined in TokensMapCallback).
	public map<T>( callback: TokensMapCallback<T> ) : T[] {

		return( this._tokens.map( callback ) );

	}

	// I map the internal tokens using the given callback.
	// --
	// NOTE: This has the same effect as the previous map<T> method, except for this uses
	// an in-line type definition for the callback. You can still see that the callback
	// is defined to return T, which can then allow for "type argument inference" for the
	// parameterization of .map2<T>.
	public map2<T>( callback: ( token: string ) => T ) : T[] {

		return( this._tokens.map( callback ) );

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

var tokens = new Tokens( [ "Sarah", "Joanna", "Tricia" ] );

// Let's map the tokens using a Callback that returns a String value.
// --
// NOTE: The :string return type of the callback will be used to infer the return
// type of the .map() method.
var greetings: string[] = tokens.map(
	( token: string ) : string => {

		return( `Hello, ${ token }.` );

	}
);

for ( var greeting of greetings ) {

	console.log( greeting );

}

// Let's map the tokens using a Callback that returns a Boolean value.
// --
// NOTE: The :boolean return type of the callback will be used to infer the return
// type of the .map2() method.
var checks: boolean[] = tokens.map2(
	( token: string ) : boolean => {

		return( token === "Joanna" );

	}
);

for ( var check of checks ) {

	console.log( check );

}

As you can see, I've actually provided two different .map() methods: one that uses an external callback Interface and one that uses an in-line callback signature. These are two slightly different approaches; but, they both work in the same way - they parameterize the callback in order to allow TypeScript to be able to infer the Type parameter of the .map() method. This allows me to invoke the .map() methods without supply an explicit Type paramater.

If we run this code through ts-node, we get the following output:

Using Type Argument Inference in TypeScript when accepting generic callbacks.

As you can see, we were able to invoke the .map() and .map2() methods without explicitly parameterizing the method call. This is because TypeScript was able to look at our Callback signature and infer the method-type parameter from the parameterized Callback return Type.

I'm a huge fan of TypeScript for the way it forces you to think about how your data is being used and consumed. But, that doesn't always mean that TypeScript is easy to write. Dealing with Generics certainly increases the complexity of the code. But, thanks to features like type argument inference, at least consuming the code can remain relatively straightforward.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel