TypeScript
2019-06-24

Associative PHP arrays in TypeScript

Feature image

The concept of associative arrays in PHP is kinda uncommon; most languages implement this concept with maps, lists, dictionaries or similar constructs. And, even if associative arrays in PHP are possible (and heavily used), they can get a bit confusing as soon as you want to work with them in your JSON API.

Associative Arrays in JSON

Let's take a look at what happens to associative arrays within JSON responses.

$data = []; 
// Becomes [] in JSON

$data = ["one", "two", "three"];
// Becomes ["one", "two, "three"] in JSON

$data = [0 => "foo", 137 => "bar"];
// Becomes { "0": "foo", "137": "bar" } in JSON

$data = ["foo", 3 => "bar", "hello" => "world"];
// Becomes { "0": "foo", "3": "bar", "hello": "world" } in JSON

As you can see only "real" arrays stay arrays in JSON, while associative items turn the array into objects.

The proper type

Since the associative array either becomes and object or an array the proper type would be object | []. It's actually pretty simple, but we can do this even better by introducing our very own type for associative arrays - including a generic to get information about the type of our array elements.

type AssociativeArray<T = unknown> = {[key: string]: T | undefined} | T[];
T could eventually be omitted, but provides additional type information for our arrays.

This union type tells TypeScript that we're either dealing with an object consisting of key-value pairs, where key is a string and the value is of type T, or a simple array with elements of T (which also includes empty arrays). This may sound a bit complicated, but if we're looking at an example again everything should be clear (assuming getData retrieves data from our API):

// $data = [];
const data: AssociativeArray = getData(); 
// not specifying `T` means that the type remains unknown

// $data = ["one", "two", "three"];
const data: AssociativeArray<string> = getData();
data[0]; // equals "one"
data[1].toFixed(); // error: `toFixed` does not exist on strings!

// $data = [0 => "foo", 137 => "bar"];
const data: AssociativeArray<string> = getData();
data[0]; // equals "foo"
data[137]; // equals "bar"

// $data = ["foo", 3 => "bar", "hello" => "world"]
const data: AssociativeArray<string> = getData();
data["0"]; // equals "foo"
data["3"]; // equals "bar"
data["hello"]; // equals "world"
data["hello"].toFixed(); // error: `toFixed` does not exist on strings!

// $data = [1, 2, 3]
const data: AssociativeArray<number> = getData();
data[0]; // equals 1

// $data = [[], [1, 2, 3]]
const data: AssociativeArray<number[]> = getData();
data[0]; // equals []
data[1]; // equals [1, 2, 3]
data[1][0]; // equals 1
data[1][0].substr(0, 5); // error: `substr` does not exist on numbers!
Thanks to the generic it's easy to tell what's inside our array.

Maps

As mentioned in the beginning many languages implement associative arrays in form of maps (or lists, or whatever you want to call them). Maps also exist in JavaScript and therefor we could also cast our JSON response to a map by simply using Object.entries:

// Data from our API
const data =  {
  "foo": "bar",
  "hello": "world"
};

const map = new Map<string, string>(Object.entries(data));

map.get('foo'); // equals "bar"
map.get('hello'); // equals "world"
Maps in TypeScript are generic and allow us to specify the type of our keys and values