Hybrid Types in TypeScript Being Dynamic With Type Checker
Yesterday I’ve been refactoring one of our internal library in GWI from JavaScript to TypeScript.
As a JavaScript veteran I like to use some edgy non-conventional & pre-ES 2015 coding styles.
I’m not using prototype
, class
or this
much often.
I’m rather using Factory Constructor Pattern
quite often as well as Higher Order Functions for simulating currying
and like to with objects and play with scopes. Now you might think that this is silly. No one will understand my code and using truly
private functions makes it harder to extend functionality. And you’re right:D Anyway I know that I can write more reliable
code more quickly and from my experience when code is reliable enough not many people will need to change it.
And when they do they are mostly experienced and are able to understand it.
Also I like private functions. If you miss any functionality or abstraction it’s always good idea to add it directly to library
or write your own than hack it. Anyway this article is not about these patterns
and how to use them but rather about how it feels when you put TypeScript in the mix.
If you want to learn more about these patterns DuckDuckGo some other article.
First let me explain one thing. I’m not writing TypeScript day to day. But when I do I’m mostly exploring edges of what it can do. I’m writing a lot of ES 2015 ECMAScript in work and Elm for fun (what is slowly changing since we already shipped first feature written in Elm elm as part of our production Ember.js app).
Problem
The idea is this. We have got our internal system for charts written in D3.js. Some parts of this are open-sourced and available on GitHub. In GWI we have got whole charts written in pure D3 that are sharing common interface so on application layer you’re basically just dynamically switching factory functions for charts based on based on chart type and everything works like a magic. However testing visualization layer is hard or even impossible. Wouldn’t it be a good idea to have at least type check for these interfaces? I think it would!
For purpose of this tutorial I’ve picked one smaller part rather than whole chart. D3 itself comes with component called d3.svg.axis. However sometimes it doesn’t fit your needs so you’ll need to implement your own solution. Lets say we want to implement custom axis which will create ticks based on data we have so for each data-point it will create a tick on axis. This is how we want to use our new component:
const axis = exactAxis()
.scale(scale)
.data(someData)
.tickFormat(d => `${d}`);
.select('.axis-group').call(axis); d3
As you can see I’ve chosen exactAxis
as a name for this component. It’s a function
. However it returns some Object
that has at least scale
, data
& tickFormat
methods. We’re using chaining so at least scale
and data
should return object they are defined on.
Also on the last line we are using d3.selection.call
which means that axis
(thing returned by tickFormat
call) needs to be function
. This might mean that tickFormat
returns some function
instead of object
but that is silly idea. Then you will need to always call tickFormat
as last method.
That said I think we can agree that:
exactAxis()
returnsfunction
(and functions are Objects in JavaScript) with all methods defined.- Each method will return object it’s called on (so we can chain method calls).
Interface
Now we know what we need so lets define interface for out exactAxis
function.
import * as d3 from 'd3';
interface TickFormat extends Function {
: string;
(any)
}
interface ExactAxis extends Function {
g : d3.Selection<any>) : void;
(scale(scale : d3.scale.Ordinal<any, any>) : ExactAxis;
data(data : any[]) : ExactAxis;
tickFormat(fc : TickFormat) : ExactAxis;
}
As you can see there are some any
used.
Maybe it’s good idea to solve this using Generics.
import * as d3 from 'd3';
interface TickFormat<T> extends Function {
data : T) : string;
(
}
interface ExactAxis<T> extends Function {
g : d3.Selection<any>) : void;
(scale(scale : d3.scale.Ordinal<any, any>) : ExactAxis<T>;
data(data : T[]) : ExactAxis<T>;
tickFormat(fc : TickFormat<T>) : ExactAxis<T>;
}
OK this is better. Now it’s obvious that we are passing some data around.
There are still some any
used for d3 parts but I think we can leave it.
So Called Hybrid Types in TS
Now we can actually start implementing our axis component. This was the part I was not familiar till yesterday. I was actually asking some friends who work with typescript daily how an interface like this can be implemented in typescript but unluckily no one knew. I knew how this can be done in JS but that implementation did not satisfy tsc (compiler). Than as all SW engineers I’ve turned my last hope to documentation and found part about Hybrid Types.
As we mentioned earlier, interfaces can describe the rich types present in real world JavaScript. Because of JavaScript’s dynamic and flexible nature, you may occasionally encounter an object that works as a combination of some of the types described above.
That’s exactly what I was looking for!
Let’s have a look at how we minimal “implementation” that satisfy our interface:
function exactAxis<T>() : ExactAxis<T> {
const axis = <ExactAxis<T>>function() {
}
.scale = function(scale : d3.scale.Ordinal<any, any>) : ExactAxis<T> {
axisreturn axis;
}
.data = function(d : T[]) {
axisreturn axis;
}
.tickFormat = function(fc : TickFormat<T>) : ExactAxis<T> {
axisreturn axis;
}
return axis;
}
That’s it! Actually this won’t do anything but it’s whole boiler plate we need. Now comes the easy part. We can just simply implement logic (and that’s always simpler than designing API, right?)
So as a bonus - This is one possible full implementation:
function exactAxis<T>() : ExactAxis<T> {
// Constants
const TEXT_DELTA : number = 25;
const WITHOUT_TEXT_DELTA : number = 12.5;
// Instance variables
let data : T[] = [];
let tickFormat : TickFormat<T> = (d) => `${d}`;
let scale;
// Render
const axis = <ExactAxis<T>>function(g) {
// D3 always returns array
// Lets render axis for every given group.
.each(function() {
gconst $el : d3.Selection<any> = d3.select(this);
// Prepare data for ticks
const ticksData = data.sort((a, b) => Number(a) - Number(b))
.reduce((acc, d) => {
const latestWithText = last(acc.withText);
// skip duplicates immediately
if (latestWithText && latestWithText === d) { return acc; }
// for first or not too close we add text one
if (!latestWithText || Math.abs(scale(latestWithText) - scale(d)) >= TEXT_DELTA) {
.withText.push(d);
accelse {
} // we add tick without text
const latestWithoutText = last(acc.withoutText);
// check for delta from latest with text
if (Math.abs(scale(latestWithText) - scale(d)) >= WITHOUT_TEXT_DELTA) {
// check for delta from latest without text
if (!latestWithoutText || Math.abs(scale(latestWithoutText) - scale(d)) >= WITHOUT_TEXT_DELTA) {
.withoutText.push(d);
acc
}
}
}
return acc;
, { withText: [], withoutText: [] } );
}
// Render
// With text
const withText = g.selectAll('.tick.tick--with-text').data(ticksData.withText);
.enter().append('g')
withText.attr('class', 'tick tick--with-text')
.append('text')
.style('text-anchor', 'middle');
.transition()
withText.attr('transform', d => `translate(${scale(d)}, 15)`)
.select('text')
.text(tickFormat);
.exit().remove();
withText
// Without text
const withoutText = g.selectAll('.tick.tick--without-text').data(ticksData.withoutText);
.enter().append('g')
withoutText.attr('class', 'tick tick--without-text')
.append('line')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', 5);
.transition()
withoutText.attr('transform', d => `trnaslate(${scale(d)}, 0)`);
.exit().remove();
withoutText;
})
}
// Public Interface
.scale = function(newScale : d3.scale.Ordinal<any, any>) : ExactAxis<T> {
axis= newScale;
scale return axis;
}
.data = function(d : T[]) {
axis= d;
data return axis;
}
.tickFormat = function(fc : TickFormat<T>) : ExactAxis<T> {
axis= fc;
tickFormat return axis;
}
return axis;
}
Note: If you read carefully you know that I’m not using this
often. However for this example has one usage of this
to get element in d3’s each
method. It doesn’t make sense to go against library API.
Note: This is really simplified implementation. Actual thing similar to this we have in our lib uses 3 types of ticks
(long text, short text, no text). This means also different interface for FormatValue
an we are also always adding
line tick (without text) in middle of ticks with text. However I think this simpler example is better for purpose of this
article.
Don’t Drink Too Much Kool-Aid
TypeScript maybe lets you express these kind of dynamic APIs but there is down side to it.
If you remove implementation for any method compiler won’t complain even though your function
does not return valid ExactAxis<T>
implementation. However if you make mistake in method implementation (change its types) it will fail
during compile time which seems as an improvement to pure JS version. That said if you want to play with something like this
It’s usually good idea to always start with boilerplate with all methods defined.
However I do like reddit as a platform so feel free to shout here:
r/typescript/comments/5h15an/hybrid_types_in_typescript