Stick with me for a few minutes, and we’ll construct a full version of this functions type, with all the bells and whistles. And don’t worry if you’ve never used shades, we’re going to build everything from scratch, and we’re going to ease into this pool, so that we don’t lose anyone. All you’ll need is a familiarity with TypeScript and some of its features.

Baby Steps

We’ll start by creating a simplified version of get . This get only takes strings representing keys and produces a new function that can take some object and extract that path from the object.

Before we jump in to typing that, we’ll create a helper type called HasKey so that stuff doesn’t get too confusing.

HasKey is a mapped type. It represents an object that has some given string K mapped to SOME value. Note that it takes a second, optional parameter V that lets us specify the type at K , but it defaults to any .

So if we wanted to define a type HasName that has a name prop mapped to a string, we could do it with:

HasKey is a very general type that doesn’t seem particularly useful at first. The trick is we can use it as a constraint in our function to guarantee that our input has the keys we need. Armed with this, we’re ready to write our first get function.

V1: Strings All The Way Down

The above function takes a string K and produces a new function that accepts any object so long as that object has K as a key. That’s the magic of HasKey; we can use it as part of an extends clause to enforce that whatever we get handed has the key we want. Then the result type is the type of the key K on S .

We can also stack this together to get nested accessors:

Nesting Dolls

Notice how we’re now using that optional second parameter of HasKey? We are specifying that our input S must be an object with some key K1 that itself is an object with some key K2 . We could keep repeating this process to guarantee any number of keys and any depth path.

S[What Now?]

That S[K] in the return type might be unfamiliar. It’s called an index type. It’s a built in feature of TypeScript that allows us to generically reference the type at a key of an object when that key isn’t known ahead of time. While it works beautifully for our above case, the bad news is it won’t be able to handle when we start mixing more abstract paths like traversals and virtual lenses into our getters.

The good news is that TypeScript provides us with the tools we need to roll our own index types that will work with whatever mix of values we need. The silver bullet is conditional types.

Conditional types are like an if-statement for types. In fact, they’re exactly that. They let you ask a question about a type, and depending on the answer, return a different type. We can use this to create our own index type called KeyAt.

KeyAt takes an object and a string and if that object has that string as a key, it returns the type at that key. If not, it just returns never, a built-in type that (as the name implies) can never exist. The fact that KeyAt can still do something when the key is missing is going to allow us to use it in cases where we know that an object will have the right keys, but TS can’t prove it without a little help. This will be the key to writing more complex get functions.

KeyAt acts like a function for types

V2: KeyAt ‘em

This version is near-identical except that it uses the same kind of nesting trick we used for HasKey with the new KeyAt. Notice that the nesting happens in the opposite direction: we ask what object is KeyAt<S, K1> and from that result we’ll extract K2 .

We’ve now created a function that takes one or two strings and produces an accessor function that can take any object with the given keys and extract the correct type from it. Already we’ve created something pretty flexible and real-world, and we deserve to give ourselves a pat on the back.

Now we get to the point where we’re really going to start cooking with gas.

Traversals

Traversals represent a way to filter a collection of objects that we encounter along our path and continue extracting values from the individual objects in the collection, glomming the results together into a collection in the output. For example, in our intro, matching was a traversal that filtered down the friends list to just the users who had gold member status, and then we were able to extract all of their names into a list. This sort of behavior is some of the most compelling in shades, so we definitely want to see how to type it.

What’s a Traversal? Well it’s just a Traversal

Traversal describes the shape of a Traversal object. The odd thing is that we don’t actually need it to contain anything. It’s just going to act as a marker, a signal to get that this position in the path is going to be a collection of Item . Because of this, we’ll be able to use the same object to handle any collection type (such as an Array, an Object, ES2015 Maps and Sets, and even Immutable.js collections). For our example, we’ll just use arrays to keep it simple (for now).

V3: A Traversal and a String Walk Into A Bar

Let’s start with a motivating example. We’ll filter down our user’s friends to those who have more than 5 friends, and then extract out all their names into a list. We’ll do the filtering with a function matching that takes a filter function from A to boolean and produces a Traversal<A> :

Let’s think about this before we dive in. We are first going to perform a traversal over our collection, which means we are filtering down the array in some way. But filtering doesn’t change the type of the output type at all, so so far, smooth sailing. But next we are going to extract out the names and so we’ll end up with a list of strings. A naïve attempt at this will run into problems:

We’re getting a never type back from our KeyAt in the return type. This is because S doesn’t represent a User object. It’s a User[]. The only keys we could extract from it are things like length , map , etc. We can fix this with a little acrobatics:

Where’s Waldo but for type signatures

Do you see the difference? We changed our type constraint S to be about the elements of the collection, and then just said that our input would be an array of S .

This works very well and is a handy trick for when we know about the structure a container is going to have (an Array in this case), and we want to constrain or refer to the element type. However it has a big downside, and what’s more, I’ve already said it in that last sentence. It requires us to know the structure the container is going to have. We want to be able to write generic functions that can work over many different types of containers such as Maps, Sets, Arrays and Objects simultaneously. And for this we need to pull out the big guns.

Unpack and Collection

Remember those conditional types we talked about earlier? We can use those to create a very powerful utility type called Unpack:

The first thing that might jump out at you is the infer keyword. This is the secret sauce that’s doing our job for us. When we’re asking a question with a conditional type, i.e. “Is this given F an array of A ?” we might not know the exact type A . infer lets us give a name to that inner type that we don’t know, but TypeScript does. Thus what Unpack does is allow us ask if a given object is one of any number of collection types (Array, Set, Map, Promise, etc.), and figure out what type is inside that collection.

This is the first step towards our get function accepting multiple types of containers as input. Next, let’s create a Container type that will encompass all of the collections that we might want to traverse over.

So now if we go back to our naïve V3 and replace all our Arrays with Collections, and put in a carefully placed Unpack, we’ll almost have something that works.

Can you see the issue with our result?

TS only knows that our output is a Collection<string, any> , but we know that it should actually be a string[] . This error is because, well, that’s exactly what we told it.

Higher Kinded Types

Let’s take a short digression and talk about why the above is a problem. Imagine you were writing a version of Array::map that would work for all sorts of different types, i.e. A[] => B[] and Map<K, A> => Map<K, B> . How would you go about it? (Psst! If you want this, it’s available in shades). The traditional OO approach would be to make an interface Mappable , and then we’d implement Mappable for all the container classes. But there’s a big catch: this’ll create the same issue we have above.

The interface Mappable doesn’t know the type of the container class that it’s going to be implemented on, so it just says that the function map will return a Mappable<B> . When we implement this on our List container class, we match the type signature to our interface and our List::map returns a Mappable<B> as well. But this means we’ve lost type information! List::map could return any other class that implements Mappable.

What’s more, this means TS doesn’t know that out is a List, so we can’t call any List methods on our output, or pass it to a function expecting a List. This is real trouble if we want to have common interfaces over data containers like Lists, Maps, Sets, etc. (N.B. TS-savvy users might notice that this exact use case can actually be fixed, but in general, TS cannot handle functions like map , and we’re going to focus on those more general problems.) What we really want is something like this:

Abstracting over containers

This idea of being able to say F is some generic container, and our function returns F<A> is called Higher Kinded Polymorphism, and is a critical part of languages like Scala and Haskell. Alas, TS does not support it (yet).

So we’re going to fake it.

Functor

The most common higher kinded type is Functor, and it’s exactly that Mappable<F, A> from above: it takes some type F<A> , an (a: A) => B function, and returns a F<B> , whatever that F was. It really just represents a type that you can call map on. As mentioned above, we can’t actually implement this in TS, but we can pick all the types that we might want to map over, and write a version of Functor that handles any one of those. How? With our old buddy conditional types:

The Poor Man’s Functor

We’re taking our type F and asking in turn if it’s any of these containers: Array, Object, Set, etc. If we get a hit, we spell out what the correct F<B> return type should be.

Final Draft

Alright, this has been a long time coming and you’ve been very patient. Let’s revisit our V3, and beef it up with our Functor:

It works!

Let’s take a look at exactly what changed. Instead of returning a Collection, we used our Functor class to figure out exactly what collection we wanted to return. Functor wants two parameters:

The entire collection object we’re transforming: S . The new member type of the collection. This is just the same as before; we want to extract the type that’s at the key K on the member items of S . We use our buddy Unpack to extract whatever is in the collection S and then use KeyAt to get the right key.

Now let’s take a step back; we’ve done this all incrementally, so it might not seem super cool yet. We have a function get that can take some generic Traversal object and a random string, and interpret those as abstract paths into some as yet unknown object. We get back a function which will take any object that matches that abstract path, interpret what that path means for this object, and construct a precise and useful return type for this use case.

It even will catch subtle errors! For example, what if we typed namez instead of name ? TS will catch it and gives a thorough breakdown of what went wrong:

u cannot haz

TS has done a tremendous job of creating a type system flexible enough to handle all of the weird tricks that us JavaScript programmers use. We just need to feel comfortable harnessing its awesome powers.