Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jonas Bučinskas
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jonas Bučinskas

Maintaining Proper Type-Checking With Callbacks Using TypeScript In Angular 2 RC1

By
Published in

A couple of days ago, I demonstrated how to get the in-browser TypeScript transpiler to enforce type-checking. In that demo, however, I could not get the type-checker to validate a variable assignment in one of my callbacks. While I was befuddled by this validation gap, Greg Lockwood was kind enough to jump in and point out that the lack of type-checking was due to my use of .bind(). Apparent, in TypeScript, bind is bad. As such, I wanted to look at ways to use callbacks while still maintaining proper type-safety.

Run this demo in my JavaScript Demos project on GitHub.

The problem with .bind(), from what I can gather in this discusion, is that it returns a value of type "any". Because of this, the type-checker can make no guarantees about how a bound method should behave. As such, in order to enforce type-checking we have to avoid the .bind() method. But, we may still need valid "this" references within of our callbacks. Which means we have to get a little fancy with how we approach the problem.

If you're a fan of the fat-arrow functions in ES6, the solution might be obvious. But, while I love the idea of the fat-arrow function, it suffers from three flaws, in my opinion:

  • It can't have a name (although you can assign it to a variable).
  • It doesn't get hoisted.
  • It can lead to overly terse code.

Hoisting, no matter what anyone has told you, is one of the most magical features of JavaScript. It just makes life better and your code easier to organize (and often read). The fact that you can't hoist a fat-arrow function means that you either have to use them inline with another expression; or, you have to define them prior to their use; both of which provide sub-optimal levels of flexibility.

That said, this isn't a post about the pros-and-cons of the fat-arrow - it's a post about type-checking. So, let's look at how can use fat-arrow functions as one of the viable ways to maintain type-checking in the TypeScript transpiler.

In the following demo, I am going to attempt to assign a string value to a numeric property. The first test will not enforce type-checking - that's our control test that uses the .bind() method. The four subsequent tests each use a different approach to make sure the type-checker catches the mismatched assignment:

NOTE: I am doing this in the context of an Angular 2 RC1 app, hence the AppComponent; but, there's no reason that this applies exclusively to Angular 2. It just so happens that this is how I am setup to run TypeScript in the browser.

// Import the core angular services.
import { Component } from "@angular/core";
import { Observable } from "rxjs/Observable";

// NOTE: Loading RxJS operators for SIDE-EFFECTS only.
import "rxjs/add/observable/of";


// I provide the root component of the application.
@Component({
	selector: "my-app",
	template:
	`
		From Observable: {{ someNumericProp }}
	`
})
export class AppComponent {

	// Notice that we are explicitly declaring this property as a number. As such, the
	// TypeScript transpiler should warn us when / if we try to store a non-numeric
	// value into this property.
	public someNumericProp: number;


	// I initialize the component.
	constructor() {

		this.someNumericProp = 0;

		this.testControl();
		this.testA();
		this.testB();
		this.testC();
		this.testD();

		// Logging out the current object so we can see what is on the instance
		// and what is on the prototype.
		console.log( this );

	}


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


	// In this "control" test, I'm using my older (and favorite) approach where I would
	// have to use the .bind() operator in order to ensure that the subscribe callback
	// had access to the appropriate THIS scope. HOWEVER, we have to be careful because
	// using the .bind() method BREAKS ALL TYPE-SAFETY CHECKS.
	public testControl() : void {

		Observable
			.of<number>( 1 )
			.subscribe( handleSubscribe.bind( this ) ) // <-- Using .bind( this )
		;

		function handleSubscribe( value: number ) : void {

			this.someNumericProp = "value"; // <-- Type incompatibility.

		}

	}


	// In this test, we're maintaining proper type-checking by using the fat-arrow
	// syntax. However, in order to do that, we've switched over to an inline function
	// expression, which I don't love.
	// --
	// NOTE: The fat-arrow keeps the proper THIS reference while allowing type-checking.
	public testA() : void {

		Observable
			.of<number>( 1 )
			.subscribe(
				( value: number ) : void => {

					this.someNumericProp = "value"; // <-- Type incompatibility.

				}
			)
		;

	}


	// In this test, we're keeping the fat-arrow function syntax; however, rather than
	// using an inline function expression, we're moving to a variable assignment. But,
	// since VARIABLE ASSIGNMENTS AREN'T HOISTED, we have to assign the fat-arrow
	// function before we use it. Which is ghetto fabulous.
	// --
	// NOTE: The fat-arrow keeps the proper THIS reference while allowing type-checking.
	public testB() : void {

		var handleSubscribe = ( value: number ) : void => {

			this.someNumericProp = "value"; // <-- Type incompatibility.

		};

		Observable
			.of<number>( 1 )
			.subscribe( handleSubscribe )
		;

	}


	// In this test, we're creating and consuming an INSTANCE METHOD that was defined
	// using the fat-arrow syntax. Unfortunately, to use this approach, the subscribe
	// handler is relatively far away from the method that is consuming it.
	public testC() : void {

		Observable
			.of<number>( 1 )
			.subscribe( this.testC_handleSubscribe )
		;

	}

	// CAUTION: Since this is a property assignment, NOT a function declaration, this
	// function is being set on the INSTANCE and NOT ON THE PROTOTYPE.
	// --
	// NOTE: The fat-arrow keeps the proper THIS reference while allowing type-checking.
	public testC_handleSubscribe = ( value: number ) : void => {

		this.someNumericProp = "value"; // <-- Type incompatibility.

	}


	// IN this test, I'm going back to my older (and preferred style) in which the
	// callback is defined below its usage (and hoisted). However, rather than using
	// the .bind() method, we're using a closed-over "self" reference to maintain a
	// proper, type-safe reference to the THIS scope.
	public testD() : void {

		var self = this;

		Observable
			.of<number>( 1 )
			.subscribe( handleSubscribe )
		;

		function handleSubscribe( value: number ) : void {

			self.someNumericProp = "value"; // <-- Type incompatibility.

		}

	}

}

Test C might be a little confusing. In Test C, the callback method is actually a method on the component itself. However, it's not a method that was declared as part of the component prototype. Rather, it was declared as property of the instance itself. That's why we see it in the "self" properties in the console.log():

Maintaining type-checking in callbacks in TypeScript in Angular 2.

As you can see, all but the control test picked up the type mismatch in the property assignment. The "right" approach is just a matter of personal preference. The only one that is substantially different from the others is Test C in that it doesn't have to create a new closure for every method call. However, because of that, it can only reference component properties - it can't reference local variables declared within the calling context. So, while it is perhaps more memory efficient, it is certainly less flexible.

On the one hand, I love that type-checking adds a good degree of "self documentation" to the code. But, getting type-checking to work can require more hoop jumping than I am used to. Of course, TypeScript is still very new to me, so there is bound to be an adjustment period.

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