Tuples vs Interfaces in TypeScript
Oct 17, 2016
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 = () => {
return [ "Morgan", "Freeman", 79, true ] as Person;
};
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 = () => {
return {
firstName: "Morgan",
secondName: "Freeman",
age: 79,
eatsMeat: true
} as Person;
};
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 = () => {
return {
eatsMeat: true,
secondName: "Freeman",
age: 79,
firstName: "Morgan"
} as Person;
};
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 = () => {
return [ "Morgan", "Freeman", 79, true ] as PersonTuple;
};
let getPerson_interface = () => {
return {
firstName: "Morgan",
secondName: "Freeman",
age: 79,
eatsMeat: true
} as PersonInterface;
};
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 theyID
,FirstName
,LastName
,Age
or were theyUnitNumber
,Street
,City
,ZipCode
.