
{"id":5276,"date":"2025-07-12T20:29:05","date_gmt":"2025-07-13T00:29:05","guid":{"rendered":"https:\/\/ikriv.com\/blog\/?p=5276"},"modified":"2025-07-12T20:29:05","modified_gmt":"2025-07-13T00:29:05","slug":"recursive-types-in-typescript-safely-typing-json-value","status":"publish","type":"post","link":"https:\/\/ikriv.com\/blog\/?p=5276","title":{"rendered":"Recursive Types in Typescript: safely typing JSON value"},"content":{"rendered":"<p>I stumbled upon the following code in production:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\ntype JSONValue = Record&lt;string, any&gt;;\r\n<\/pre>\n<p>This sort of does the job, but <code>any<\/code> is a fruit of a poisonous tree: it should have had a much more ominous name like <code>cheating<\/code> or <code>TypeSystemHole<\/code>.<\/p>\n<p>Here&#8217;s what I mean:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\ntype JSONValue = Record&lt;string, any&gt;;               \/\/ looks legit\r\nconst x: JSONValue = { error: new Error(&quot;boo!&quot;)};   \/\/ hm... are classes allowed in JSON?\r\nconst y: number = x.error;                          \/\/ trust me, this compiles; and y is not a number\r\nconsole.log(y);\r\n<\/pre>\n<p>Can we define JSON in some better way?<\/p>\n<p>A na\u00efve attempt fails: a type cannot reference itself directly.<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\n\/\/ Error: Type alias &#039;JSONValue&#039; circularly references itself.\r\ntype JSONValue = null | string | number | JSONValue&#x5B;] | Record&lt;string, JSONValue&gt;; \/\/ this does not compile\r\n<\/pre>\n<p>Some sources (e.g. <a href=\"https:\/\/stackoverflow.com\/a\/47842314\">this SO answer<\/a>) suggest that Typescript types cannot be recursive at all, but it is not true.<br \/>\nThere are at least two possibilities for recursion with types: recursive properties and referencing yet undefined types:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\n\/\/ recursive property\r\ntype Tree = {\r\n  value: number;\r\n  children: Tree&#x5B;];\r\n}\r\n\r\n\/\/ referencing yet undefined type\r\ntype Data = number | string | DataArray;\r\ntype DataArray = Data&#x5B;];\r\n<\/pre>\n<p>The following code defines strongly typed JSON structure without resorting to <code>any<\/code> or <code>unknown<\/code>:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\n\/\/ helper type\r\ntype JSONPrimitive = string | number | boolean | null;\r\n\r\n\/\/ main type; use JSONValue to describe valid JSON values\r\ntype JSONValue = JSONPrimitive | JSONArray | JSONObject;  \r\n\r\n\/\/ another helper type\r\ntype JSONArray = JSONValue&#x5B;];\r\n\r\n\/\/ yet another helper type\r\ntype JSONObject = {\r\n  &#x5B;key: string]: JSONValue;\r\n};\r\n\r\n\/\/ some examples\r\nconst j1: JSONValue = null;\r\nconst j2: JSONValue = 10;\r\nconst j3: JSONValue = false;\r\nconst j4: JSONValue = &quot;foobar&quot;;\r\nconst j5: JSONValue = &#x5B;1, 2, &quot;3&quot;];\r\nconst j6: JSONValue = { x: null, y: false, z: &#x5B;1,2,3], t: { foo: 42, bar: null }};\r\n\r\n\/\/ some counter-examples\r\nconst not_j1 : JSONValue = { x: new Error(&quot;boo&quot;)}; \/\/ this fails to compile\r\n<\/pre>\n<p>In conclusion, you don&#8217;t need to resort to <code>any<\/code> to define a recursive type like <code>JSONValue<\/code>: use recursive types instead. Using <code>any<\/code> opens a gaping whole in the type system and is therefore, not recommended.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I stumbled upon the following code in production: type JSONValue = Record&lt;string, any&gt;; This sort of does the job, but any is a fruit of a poisonous tree: it should <a href=\"https:\/\/ikriv.com\/blog\/?p=5276\" class=\"more-link\">[&hellip;]<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"Layout":"","footnotes":""},"categories":[29],"tags":[],"class_list":["entry","author-ikriv","post-5276","post","type-post","status-publish","format-standard","category-typescript"],"_links":{"self":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5276","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5276"}],"version-history":[{"count":8,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5276\/revisions"}],"predecessor-version":[{"id":5284,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=\/wp\/v2\/posts\/5276\/revisions\/5284"}],"wp:attachment":[{"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5276"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5276"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ikriv.com\/blog\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5276"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}