Hackle's blog
between the abstractions we want and the abstractions we get.

Check out my workshops at NDC { Minnesota }
Nov 15-16 TypeScript Power Users
Nov 17-18 Simple by Design: Declutter Your Architecture, Code and Test

Dependent types in TypeScript?

(Update: more proper dependent types have become possible with new language features in TypeScript, please see this later post).

"A dependent type is a type whose definition depends on a value", according to Wikipedia. Unlike most statically-typed programming languages where types and values live in different places, with dependent types, they may appear together. For example, in Idris, there is a function that appends a Vector to another, with a type signature as below,

append : (xs : Vect m a) -> (ys : Vect n a) -> Vect (m + n) a

Whereas m and n are values that specify the lengths of the Vectors. As they are values, it's only natural to treat them as such, so the result Vect would have length m+n.

You will be surprised, but it's possible to get a taste of dependent types in TypeScript, just from a slightly different direction.

Literal types, come to think of it, are basically values. And with TypeScript's support for conditional types, we can (almost) manage a little type calculation.

Let's get to it!

First we have a union type comprises two literals, and a function toggle.

type OnOff = 'on' | 'off';

function toggle(st: OnOff): OnOff {
    return st === 'on' ? 'off' : 'on';
}

This is all very well but nothing is stopping us from returning 'on' for 'on' and 'off' for 'off', therefore voiding the promise of toggle. Is there a way to express "toggle" on type level so 'on' is guaranteed to be 'off' after toggling, and vice versa?

This calls for some conditional typing. Introduce Toggle (as a type).

type Toggle<T extends OnOff> = T extends 'on' ? 'off' : 'on';

Why, it's only a good-old ternary expression, albeit on type level. See how both 'on' and 'off', innocent string values, live comfortably in type declarations?

Pretty, you say, but does it work?

const state1: OnOff = 'on';
const state2: Toggle<typeof state1> = 'off'; 
const state3: Toggle<typeof state1> = 'on'; // error: Type '"on"' is not assignable to type '"off"'.ts(2322)

By assigning type Toggle<typeof state1>, the compiler forces state2 to have value "off", and rejects state3 for violating its type!

However, I am out of luck when implementing toggle:

function toggle<T extends OnOff>(st: T): Toggle<typeof st> {
    return st == 'on' ? 'off' : 'on';
}

// error: Type '"on" | "off"' is not assignable to type 'Toggle<T>'.
//  Type '"on"' is not assignable to type 'Toggle<T>'.ts(2322)

The problem, I guess, is the compiler cannot massage 'on' | 'off' back to Toggle<typeof st>, although clearly they are equal when we compare their eventual constituents.

Despair not, as is usually the case, we can cheat with as any,

function toggle<T extends 'on' | 'off'>(st: T): Toggle<T> {
    return st == 'on' ? 'off' : 'on' as any;
}

const state4 = toggle('on');

And if we inspect the type of state4, it will be "off".

If you find this interesting, maybe you'd also like to read about this issue on GitHub.

P/S I won't recommend trying something like the append example, just yet. (update: But we will, right here)