Recursive Types in Typescript: safely typing JSON value

I stumbled upon the following code in production:

type JSONValue = Record<string, any>;

This sort of does the job, but any is a fruit of a poisonous tree: it should have had a much more ominous name like cheating or TypeSystemHole.

Here’s what I mean:

type JSONValue = Record<string, any>;               // looks legit
const x: JSONValue = { error: new Error("boo!")};   // hm... are classes allowed in JSON?
const y: number = x.error;                          // trust me, this compiles; and y is not a number
console.log(y);

Can we define JSON in some better way?

A naïve attempt fails: a type cannot reference itself directly.

// Error: Type alias 'JSONValue' circularly references itself.
type JSONValue = null | string | number | JSONValue[] | Record<string, JSONValue>; // this does not compile

Some sources (e.g. this SO answer) suggest that Typescript types cannot be recursive at all, but it is not true.
There are at least two possibilities for recursion with types: recursive properties and referencing yet undefined types:

// recursive property
type Tree = {
  value: number;
  children: Tree[];
}

// referencing yet undefined type
type Data = number | string | DataArray;
type DataArray = Data[];

The following code defines strongly typed JSON structure without resorting to any or unknown:

// helper type
type JSONPrimitive = string | number | boolean | null;

// main type; use JSONValue to describe valid JSON values
type JSONValue = JSONPrimitive | JSONArray | JSONObject;  

// another helper type
type JSONArray = JSONValue[];

// yet another helper type
type JSONObject = {
  [key: string]: JSONValue;
};

// some examples
const j1: JSONValue = null;
const j2: JSONValue = 10;
const j3: JSONValue = false;
const j4: JSONValue = "foobar";
const j5: JSONValue = [1, 2, "3"];
const j6: JSONValue = { x: null, y: false, z: [1,2,3], t: { foo: 42, bar: null }};

// some counter-examples
const not_j1 : JSONValue = { x: new Error("boo")}; // this fails to compile

In conclusion, you don’t need to resort to any to define a recursive type like JSONValue: use recursive types instead. Using any opens a gaping whole in the type system and is therefore, not recommended.

Leave a Reply

Your email address will not be published. Required fields are marked *