James Monger Computers, cars, cynicism

Tuples vs Interfaces in TypeScript

A tuple is effectively an array with specific types and a specific length. They’ve been supported in TypeScript since TypeScript 1.3 (it’s now 2.0) and I’m going to do a quick investigation about when they should be used (if anywhere!)

As a tuple can’t have any behaviour (it is just a data type), I’m going to compare it to an interface, although you could just as easily use a class.

You can see the example code for this post on GitHub at jameskmonger/tuples-vs-interfaces.

Readability and maintainability

Obviously an important part of your code is how maintainable it is. Code which is just thrown together may be quicker in the short term (although with a well-setup stack, it should be quicker to write good clean code!) but in the long term it will come back to bite you. Let’s take a look at the different levels of readability/maintainability between tuples and interfaces.

An example would be a getPerson function which gives us a person - it can just be some constant data: their first name, surname, age and whether or not they eat meat. We can do this nice and easily with a tuple!

let getPerson_tuple: () => [string, string, number, boolean] = () => {
    return [ "Morgan", "Freeman", 79, true ];
};

To make it a little bit better, we can use a type alias in order to prevent having to manually define the tuple:

type Person = [string, string, number, boolean];

let getPerson_tuple: () => Person = () => {
    return [ "Morgan", "Freeman", 79, true ];
};

It’s not too bad to use, right? Fairly easy to return a Tuple. What about using it? Let’s set up a quick benchmark test with Benchmark.js.

suite.add("tuple", () => {
    let person = getPerson_tuple();
    let fullName = person[0] + " " + person[1];
})

When inspecting the type of person with your IDE, we only know that indices 0 and 1 are string types - we don’t have any idea about what they represent. What about if we used an interface? Would it be easier then?

interface Person {
    firstName: string;
    secondName: string;
    age: number;
    eatsMeat: boolean;
}

let getPerson_interface: () => Person = () => {
    return {
        firstName: "Morgan",
        secondName: "Freeman",
        age: 79,
        eatsMeat: true
    };
};

Okay, a bit more lengthy to create one, but you don’t need to remember the correct order to put the values in. You can put them in any order without changing the functionality!

let getPerson_interface: () => Person = () => {
    return {
        eatsMeat: true,
        secondName: "Freeman",
        age: 79,
        firstName: "Morgan"        
    };
};

How’s the readability for an interface?

suite.add("interface", () => {
    let person = getPerson_interface();
    let fullName = person.firstName + " " + person.lastName;
})

Much easier to read and understand. You can look at the code and immediately understand what’s going on. You don’t need to wonder “why is person an array?” or “is there an index 3?” Also, you get some nice intellisense around person so you know that it contains an age and an eatsMeat, rather than just knowing that there’s a number and a boolean on it.

Round one to interfaces!

Performance

Luckily we started making a benchmark suite in the first chapter, so let’s wrap this up and run it, and see what we get.

import { Suite } from "benchmark";

type PersonTuple = [string, string, number, boolean];
interface PersonInterface {
    firstName: string;
    secondName: string;
    age: number;
    eatsMeat: boolean;
};

let getPerson_tuple: () => PersonTuple = () => {
    return [ "Morgan", "Freeman", 79, true ];
};

let getPerson_interface: () => PersonInterface = () => {
    return {
        firstName: "Morgan",
        secondName: "Freeman",
        age: 79,
        eatsMeat: true
    };
};

new Suite()
    .add("tuple", () => {
        let person = getPerson_tuple();
        let fullName = person[0] + " " + person[1];
    })
    .add("interface", () => {
        let person = getPerson_interface();
        let fullName = person.firstName + " " + person.secondName;
    })
    .on("cycle", (event) => {
        console.log(String(event.target));
    })
    .on("complete", function() {
        console.log("Fastest is " + this.filter("fastest").map("name"));
    })
    .run({ async: true });

Running this on Windows 10 Pro, processor Intel i5-4750 3.2 GHz, RAM 12.0 GB, with Node v6.8.0, I get the following outputs:

λ node index.js
tuple x 69,527,096 ops/sec ±2.87% (86 runs sampled)
interface x 102,375,602 ops/sec ±1.27% (87 runs sampled)
Fastest is interface

Round two to interfaces. Game, set, match!

Conclusion

Not only is using an interface rather than a tuple more readable, but it’s also much more performant (or so it seems!)

Tuples could be good if you are interacting with an API for example which provides you an array of different types (whether you like it or not) but you could (should!) use an adapter pattern in order to turn the horrible nameless array entries into an object with named properties.

To summarise, I think that Matthew Whited explains it well in his StackOverflow answer:

Tuples can be useful… but they can also be a pain later. If you have a method that returns Tuple<int,string,string,int> how do you know what those values are later. Were they ID, FirstName, LastName, Age or were they UnitNumber, Street, City, ZipCode`.