Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Dan Wilson
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Dan Wilson

Defining Functions With Properties Using TypeScript Declaration Merging In Angular 2.4.9

By
Published in

In the last few days, I've taken a look at consuming the Zendesk web widget API in an Angular application. Aside from the apparent race condition, the thing that makes the Zendesk web widget interesting is that it exhibits a dual nature: before the API has fully-loaded, the zEmbed() Function acts as a queueing mechanism for callbacks. Then, once the API is fully-loaded, the zEmbed object presents as an API for interacting with the rendered widget. This dual-nature is a pattern that I've seen several times in 3rd-party JavaScript libraries; but, I wasn't sure how something like this would be modeled in TypeScript. After a little research, the solution appears to be a TypeScript feature known as "declaration merging."

Run this demo in my JavaScript Demos project on GitHub.

In my previous Zendesk web widget experiments, I didn't have to write the zEmbed Function / Object, I just had to provide a Type declaration for it so that the TypeScript compiler wouldn't complain. This Type declaration looked like this:

// The zEmbed function is a global object, so we have to Declare the interface so that
// TypeScript doesn't complain. The zEmbed object acts as both a pre-load queue as well
// as the API. As such, it must be invocable and expose the API.
declare var zEmbed: {
	// zEmbed can queue functions to be invoked when the asynchronous script has loaded.
	( callback: () => void ) : void;

	// ... and, once the asynchronous zEmbed script is loaded, the zEmbed object will
	// expose the widget API.
	activate(): void;
	hide(): void;
	identify(): void;
	setHelpCenterSuggestions(): void;
	show(): void;
}

Here, you can see that the zEmbed object is both invocable and consumable as an API.

To create a similar duality in TypeScript, we can use a feature known as "declaration merging". Declaration merging is the process by which two or more Type declarations are aggregated to represent a single Type. TypeScript supports several ways to merge declarations; but, for this particular exploration, we can merge a "Function" and a "Namespace". In this case, the Function would be our zEmbed() function and the Namespace would hold our API methods:

// The zEmbed object for the Zendesk web widget has a dual nature. It is both an
// invocable Function and, later on its life-cycle, an object with properties that
// represent the API. In TypeScript, we can model this kind of duality by using
// "Declaration Merging". This feature allows two or more separate declarations to
// aggregate the overall definition of a particular value. One of the supported
// merge operations is a "Function" and a "Namespace" merge.

// Here, the original zEmbed() function is acting as the "Function" in our TypeScript
// declaration merge.
export function zEmbed( callback: zEmbedCallback ) : void {

	console.log( "zEmbed() provided with callback..." );

}

// ... then, we can create a Namespace that declared the Properties on our zEmbed
// Function. The exported properties in this namespace will actual be injected into
// the zEmbed() "value declaration" above.
export namespace zEmbed {

	// Exports as zEmbed.hide().
	export function hide() {

		console.log( "zEmbed.hide() called..." );

	}

	// Exports as zEmbed.show().
	export function show() {

		console.log( "zEmbed.show() called..." );

	}

}

interface zEmbedCallback {
	(): any;
}

When the TypeScript compiler processes this file, it ends up injecting the zEmbed Namespace properties into the zEmbed Function. In fact, if we look at the compiled source code, we can see exactly how this happens:

// Here, the original zEmbed() function is acting as the "Function" in our TypeScript
// declaration merge.
function zEmbed(callback) {
	console.log("zEmbed() provided with callback...");
}
exports.zEmbed = zEmbed;
// ... then, we can create a Namespace that declared the Properties on our zEmbed
// Function. The exported properties in this namespace will actual be injected into
// the zEmbed() "value declaration" above.
(function (zEmbed) {
	// Exports as zEmbed.hide().
	function hide() {
		console.log("zEmbed.hide() called...");
	}
	zEmbed.hide = hide;
	// Exports as zEmbed.show().
	function show() {
		console.log("zEmbed.show() called...");
	}
	zEmbed.show = show;
})(zEmbed = exports.zEmbed || (exports.zEmbed = {}));

As you can see, the zEmbed() function is being passed into an immediately-invoked Function expression (IIFE, pronounced "iffy"), that injects the properties of the "Namespace" into the Function object. One of the coolest features of JavaScript is that Functions can have properties.

To demonstrate that this actually works, I created a tiny Angular 2 component that just imports the zEmbed object and tries to consume it both as a Function and as an API provider:

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

// Import the application components and services.
import { zEmbed } from "./zembed.service";

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.css" ],
	template:
	`
		<p>
			<em>Experimenting with Namespace declaration merging.</em>
		</p>
	`
})
export class AppComponent {

	// I initialize the app component.
	constructor() {

		// Try consuming the zEmbed() object as an invocable Function.
		zEmbed(
			() => {
				// ...
			}
		);

		// Try consuming the zEmbed object as an API surface area.
		zEmbed.show();
		zEmbed.hide();

	}

}

When we run this code, we get the following page output:

Using TypeScript declaration merging to create Functions with Properties.

As you can see, the imported zEmbed reference was consumable both as an invocable Function and as an Object with Properties. The two different type declarations were successfully merged and with nary a TypeScript compiler error in sight.

One of the things that I like most about TypeScript is simply the fact that it forces me to really think about how values get defined and about what assumptions can be made by consuming code. And, it's great to see that TypeScript supports constructs like Declaration Merging that help model real-world JavaScript usage. In this case, it makes it easy to define Functions that also act as Objects.

Special thanks to the StackOverflow post that pointed me in the right direction.

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