Schlez
2019

Stringly and Strongly typed TypeScript

Teaching the compiler to understand strings in TypeScript.Published .

There's a rational fear coming from JavaScript to TypeScript. The thought of having to mess with static types instead of focusing on runtime code sounds annoying. For coders who know static typings from languages with inferior type systems, like Java, or languages with awesome, yet different type systems, like Reason, OCaml, Rust and Swift (which are all examples to the Hindley–Milner type system), static typings might sound very off from the standard way of JavaScript-ing.

In turns out, that TypeScript can express very complicated types, thanks to its conditional types, literals and generics. In this short post we're going to explore how to use typed string literals to have a safe interfaces to objects. This approach is the underlying mechanism of wrapping libraries like lodash, ramda and my recent @soundtype/commander and @soundtype/eventemitter, all in a type safe manner.

Stringly-typed

Consider the following JavaScript function:

function prop(obj, name) {
  return obj[name];
}

// Usage:

const user = { firstName: "Gal", lastName: "Schlezinger" };
const firstName = prop(user, "firstName");

The prop function above looks like something we can't type in a static type system. It takes an object, and then a string, which is the property name, and returns an object. A naive implementation in TypeScript would be probably something like:

function prop(obj: object, name: string): any {
  return obj[name];
}

This is, of course, a very unsafe function. How can we make it better? Let's look at a simpler function, and try to type it first. We'll talk about the identity function, that gets an object and returns the same object. Or in JavaScript:

function identity(x) {
  return x;
}

Such a simple function! How would we write it in TypeScript? The naive approach, again, would look something like:

function identity(x: any): any {
  return x;
}

But now, once we use identity(10), we get the any type back. This is not what we want. To ensure we get the same type as we provided, we need to make the function generic. Now, whenever we call the identity function, TypeScript will also provide the type of the argument, whether by inference or explicitly by usage. This is how it looks like:

function identity<T>(x: T): T {
  return x;
}

// by inference
const inferred = identity(10); // number

// explicitly
const explicit = identity<number>(10); // number

So, in conclusion, generics can help us leverage the underlying types whenever for type safety.

How do we use generics in a function like prop? Let's start by adding a generic to the object we want to take data from:

// This won't compile in strict mode!

function prop<T>(obj: T, name: string): any {
  return obj[name];
}

This implementation won't compile, because we can't index an unknown object (just a generic T) over a string. Maybe that's an array? Also, we don't know what the result type might be, so this is not good enough. A good utility TypeScript provides us is the way to ask the type of the keys of an object. This is done by using keyof Type, so keyof { hello: number, world: string } would result in the 'hello' | 'world' type, which is a union of 2 string literals.

We can leverage this by using it instead of asking for a string:

function prop<T>(obj: T, name: keyof T): any {
  return obj[name];
}

Now, our function compiles, but we still don't know what type we will get in return, and as we said before, returning an any is not acceptable.

The cool trick we can do, is to add another generic, that will extend the keyof T, let's call it Key. Then, we can access the return type by returning T[Key]:

function prop<T, Key extends keyof T>(obj: T, name: Key): T[Key] {
  return obj[name];
}

Now, the function is truly generic, and will work as expected:

prop({ hello: "world", year: 2019 }, "year"); // number
prop({ hello: "world", year: 2019 }, "hello"); // string
prop({ hello: "world", year: 2019 }, "missing"); // compilation error

This assures us we won't have any runtime undefineds, by letting our compiler be smarter and look for mistakes we might do. This tiny example can be used all over the place to provide great developer experience for library users.

The TypeScript type system also allows recursive types, conditional types and much more awesome shenanigans, we might explore in the future.

Read more