Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Nathan Deneau
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Nathan Deneau

Defining Function And Callback Interfaces In TypeScript

By Ben Nadel on

Before learning Angular 2, I had never looked at TypeScript. And now, with Angular 2, I've still not sat down to read the manual - I've just been sort of learning TypeScript as I go, looking up TypeScript features as needed. One TypeScript feature that tripped me up recently was defining an interface for a Function or a Callback. Typically, when I'm writing Angular 2, I'm defining interfaces for complex data types; but, I ran into a situation where one method accepted another method and I didn't know how to "type" that callback argument properly. So, I figured I would share what I learned about defining Function / Callback interfaces in TypeScript.

A couple of weeks ago, I blogged about injecting "Newable" classes using Angular 2's dependency-injection framework. Doing this required an interface that defined a "new" method that returns the constructed type:

  • export interface INewableService {
  • new(): Service;
  • }

Defining the interface for a Function or Callback is actually very similar to this; in fact, all you do is omit the "new" keyword, leaving in the callback signature and the return type. For example, here's the interface for a typical Node.js-style callback that accepts an error and / or a result:

  • interface ICallback {
  • ( error: Error, result?: number ) : void;
  • }

The "?" on the "result" argument allows me to invoke the callback with only an "error" argument so that I don't have to explicitly pass "null" in as a result. To see this in action, I created a silly demo in which we generate a random number and consider any random number below 0.5 to be an "error":

  • // Here we are defining an interface for a Function that accepts two arguments and
  • // returns nothing (in this case, it's the typical Node.js callback pattern).
  • interface ICallback {
  • ( error: Error, result?: number ) : void;
  • }
  • // I generate a random number and pass it to the given callback which is expected to
  • // uphold the ICallback interface (ie, accepts error or result).
  • function getRandomNumber( callback: ICallback ) : void {
  • var value = Math.random();
  • ( value >= 0.5 )
  • // Invoke callback as result callback.
  • ? callback( null, value )
  • // Invoke callback as error callback (no result).
  • : callback( new Error( "Oops, random number too low." ) )
  • ;
  • }
  • // Now, let's test our random number generator a couple of times to get random results.
  • for ( var i = 0 ; i < 10 ; i++ ) {
  • getRandomNumber(
  • ( error: Error, result: number ) : void => {
  • console.log( `${ i }:`, ( result || error.message ) );
  • }
  • );
  • }

As you can see, our getRandomNumber() function accepts an argument that upholds the ICallback interface. And, from within the getRandomNumber() function, we invoke said callback using both possible signatures.

When we run this demo, we get the following output:

0: Oops, random number too low.
1: 0.6030549010482515
2: Oops, random number too low.
3: Oops, random number too low.
4: 0.6602143379316834
5: Oops, random number too low.
6: 0.516175969303077
7: Oops, random number too low.
8: 0.517515015041822
9: Oops, random number too low.

In this case, I defined the Callback / Function interface using an explicit "interface" construct. But, this is not technically necessary. You can - technically - define the callback interface inline with the rest of the code. For example:

  • // It is also technically possible to define a Function / Callback signature inline
  • // by using the (() => X) syntax; but, for the sake of readability, please please please
  • // don't do this -- don't be that guy.
  • // Example: As an argument (to takeCallback() function).
  • function takeCallback( callback: ( error: Error, result?: number ) => void ) : void {
  • // ...
  • }
  • // Example: As a return value (from buildCallback() function).
  • function buildCallback() : ( error: Error, result?: number ) => void {
  • return callback;
  • function callback( error: Error, result: number ) : void {
  • // ...
  • }
  • }

Here, you can see that I'm defining the callback interface as an argument, first, and then as a return type, second, using the "() => return" notation.

Clearly - or, at least, I think it's clear - you don't want to use this "inline" approach. Having a function signature inside or next to another function signature is just not readable. So, I included this example for the sake of comprehensiveness, but definitely not as a recommendation. I would recommend using the explicit "interface" construct which is both more readable and flexible (in so much as it can be exported and consumed by other modules).

Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

I think your tl;dr was the last sentence's paragraph. Am I reading that right? Was the point of this blog post to say, "Don't use the lambda syntax with interface constructs so that they can be exportable to other modules?"

Might I ask where you are using TypeScript currently? I'm interested in the intersection between TS and Angular2. Do you use them concurrently or do you see them as doing basically the same function in your code?

(Also, the link below the comment box about 'unrelated questions' is broken...)

Reply to this Comment


My caution about using the "inline" function type definition was more of an aside than a tl;dr (if that's what you're referring to). The standalone interface is - I think - easier to read and is also exportable making it more flexible in consumption.

But, ultimately, the goal of the post was simply to explain how to define an interface for a callback or function, which is not something that was immediately obvious to me.

As far as TypeScript, I use it in all aspects of my Angular 2 code. Though, you can write Angular 2 code without TypeScript. In fact, when I first started digging into Angular 2, I wrote my code using vanilla ES5 and RequireJS as my module loader. So, they don't really do the same thing - they more work hand-in-hand.

I ended up switching from ES5 to TypeScript because it made the dependency-injection easier. And, once I switched to TypeScript, I found that it actually made the code shorter (for the most part). But, more than anything, I think what I love about TypeScript is that it forces me to think about how classes and data are going to be used. JavaScript is super dynamic, which allows you to do things that are perhaps unexpected - TypeScript tempers that dynamic nature with some checks and balances to make sure that you write code that behaves "as expected" by other developers.

Reply to this Comment

Hey Ben, the code snippets are not easily read in mobile, . You can't scroll to the right and see the entire code snippet. Using chrome on iPhone 7 Plus

Reply to this Comment


I have often considered how to approach this problem. Ultimately, I can't think of a way to make "code" more consumable on a mobile device - I just don't think it lends itself well to the size of the screen. I find this to be a universal problem - it's rare that I even try to read about programming on my device as I find it so unmanageable.

When I come across something that I think make the most sense, I will definitely try to improve the rendering, to be sure.

Reply to this Comment

It might be worth reviewing the Angular style guide for interfaces. They have some suggestions on naming. All those "I"s drive me crazy.!#03-03

Reply to this Comment


I don't feel strongly. I use the "I" just to differentiate a Class/Type from an abstract contract. Meaning, an interface doesn't compile down to any code - it's just used for contract validation. Unlike a Class, which compiles down to an actual JavaScript constructor function.

One benefit of having the "I" is that I can't "easily" swap out an Interface for a Type. Meaning, I can't suddenly replace:

interface Foo { }

... with:

class Foo { }

... which I think makes my changes more intentful. Switching from an Interface to a Class may require additional thinking and an evaluation of how the code is used.

But, like I said, I honestly don't feel very strongly about it.

Reply to this Comment

When I first saw about Typescript for Angular 2, I wasn't sold on it. My gut reaction was very hesitant to have to write in something other that JavaScript that then became JavaScript. I didn't GWT or Coffeescript or the like in my Angular world. Then I watched some of the videos from last year's ng-conf (had brown bag sessions with my team watching 1 or 2 a day) and saw a lot of the value in "why" for Typescript, and how it's a super set of JS, not just something that becomes JS. Those couple of sessions made me a lot more interested in it. Then using it sold me on writing with it over plain vanilla ES5. There's some things with great value like this, and others that just make me feel good, like setting up my models and having autocomplete for data returned from APIs. I haven't gotten too deep into it, outside of figuring out how to do things I want to, but I enjoy using Typescript in my Angular. It's one of those items that is in my queue to just set aside time to dig into Typescript on it's own to find more power *insert Tim Allen grunt here*

Reply to this Comment


I totally agree. When I first saw TypeScript, my initial reaction was like, "Ugg, one more thing I have to learn in order to write *JavaScript*." I actually only started writing in it so that I could break my files up into different modules without using AMD ... though, in retrospect, I think I could have done that with System.js regardless of whether or not I was using TypeScript. At the time, however, I didn't know enough to differentiate.

The first thing that really sold me on TypeScript was the way it facilitated the dependency-injection - that I could use Class references as DI tokens. That was so much easier than using the .parameters array on the Class constructor.

After that, I just found that the more I used it, the more I liked it. I like that it forced me to think about how data was going to be used and what assumptions I could, or more importantly, could NOT make about the data.

One of my favorite things about TypeScript right now is that I can use an Interface declaration to describe arbitrary data structures. So, rather than having to create a whole Class and jump through those hoops, I can just say, this piece of data has interface X. And kablamo, type-safety is in place!

I've been using that a lot to define complex arguments. So, for example, if I have a method that takes an options map:

public doSomething( config: ConfigArg ) : void { ... }

... I can then just apply some simple interface validation:

export interface ConfigArg {
. . . id: number;
. . . thing: string;
. . . thang: string;
. . . createdAt: number;

Just something so nice about that, to me.

Reply to this Comment


I whole heartily agree with that feeling. I really like using it for defining my arguments and the like. When I learned to code in college, my first CS course was in Java, so it I guess feels "familiar" in that point. I had some programming before that, a C++ course in high school and self taught HTML and whatever you code on a TI-83 and TI-89 calculators, but that was my first "real" experience. I like the more lightweight but safe feel it gives me.

It makes me want to go deep to see what more it can give me, but from the little bits alone that I'm using it, it's worth it.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.