Type-safe tuple hierarchies in TypeScript

I have a love-hate relationship with type safety. Type Checkers can be amazingly powerful tools when they work and yet can be frustrating every now and then when you hit some unsupported edge case.

Thankfully more and more of these corners are getting eliminated with the hard work that the typescript team has been putting in.

One such case that was a PITA was support (or lack thereof) for recursive types in TypeScript.

Let’s say you despise XML/JSX and want to express User interface composition as a composition of tuples.

if you are coming from any of lisp-family languages you are probably already habituated to s-expressions for composing interfaces. Hiccup, for instance, is a cool
templating language that utilizes s-expressions for consize boilerplate-free template composition.

So, now, how do we make this type safe.

Lets try the obvious approach:

Until the last stable version of typescript this would have failed because recursive type aliases were not well supported. So we’d be greeted with an error:

Hmm… let us see if we can use utilize the typical trick of using interfaces to work around this:

This indeed seems to work:

TS complains that the second assignment is not valid because:

Type ‘”di”‘ is not assignable to type ‘”div” | “span”‘.

There is a caveat though:

This passes too !

Why doesn’t TypeScript complain ? Because our interface just cares about 0|1|2 indexes. If our tuple has other irrelevant things at other indexes, it doesn’t matter – it still satisfies the ElementTuple interface.

This is a problem for us if we want to support a variable arity tuples for convenience:

In such cases, typescript’s inference can fallback to type that matches some index and ignore the other indexes (having erroneous values). This can compromise with type safety:

This passes without hiccups ! Why does TypeScript allow random attributes for a div ? Because the nested array can’t match the ElementAttrs2Tuple but can match Element1Tuple so TS doesn’t complain.

Lets see, if we can do something to restrict the other index positions:

Nope. This doesn’t work because:

An index signature parameter type cannot be a type alias.

Fortunately, there is a simpler solution to this:

We really just need care about the last position. Because an attempt to create an array literal with anything at index 3 will fail to match Element3Tuple (including undefined, null etc.) So the array size has to match exactly 3.

We can extend this to the other tuple definitions:

Note that the trailing never property has to always be optional otherwise we will end up with a type that is impossible to match.

The element tuple that we have now is very close to what we wanted. Go ahead, try it out in the playground:

While this may be cool if you like fiddling around the edges of type systems, we have to admit that though we worked around the limitations of typescript, what we ended up with is neither elegant nor intuitive.

However, with TypeScript 3.7, these workarounds are no longer needed ! Recursive types are fully supported now. This means, we can simply do:

Cool, consize and minimal. Works too !