{"id":"l-at-3-5","courseId":"advanced-typescript","lang":"en","kind":"lesson","summary":"Write recursive types for trees, JSON and deep transformations, understand the compiler’s depth limits, and decide when a clever type costs more than the bug it prevents.","html":"<p>Cartwheel’s product categories form a tree: Kitchen contains Cookware, which contains Pans. The API returns that tree as nested JSON, and the checkout’s settings screen edits a deeply nested config object. Both need types that refer to themselves. TypeScript handles that well, up to a point. This lesson covers how to write recursive types, where the limits are, and the judgment call this whole section has been building towards: when to stop.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Types that refer to themselves</h2>\n<p>A type alias may mention itself inside an object or array type:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Category</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">children</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Category</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[] };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Json</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Json</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">|</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">key</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Json</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span></code></pre></div>\n<p><code dir=\"ltr\">Category</code> describes a tree of any depth. <code dir=\"ltr\">Json</code> describes any value <code dir=\"ltr\">JSON.parse</code> can return, which makes it a much better return type for a parser than <code dir=\"ltr\">any</code>: it is honest, and it forces you to narrow before use.</p>\n<p>Recursive <em>type-level functions</em> follow the same idea. <code dir=\"ltr\">Awaited&lt;T&gt;</code>, built into the lib since TypeScript 4.5, unwraps promises until there are none left. A simplified version:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyAwaited</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">infer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> V</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyAwaited</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">V</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Loaded</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> MyAwaited</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Promise</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Order</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>>>; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Order</span></span></code></pre></div>\n<p>Each step peels off one <code dir=\"ltr\">Promise</code> and calls itself on what was inside. When the type is no longer a promise, the false branch returns it and the recursion ends.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Deep transformations</h2>\n<p>The mapped types from <a href=\"/lesson/advanced-typescript/l-at-3-2\">Mapped Types and Key Remapping</a> are shallow. Recursion makes them deep:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepReadonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">args</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> object</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepReadonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> FrozenCheckout</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepReadonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Checkout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>;</span></span></code></pre></div>\n<p>Each case is there for a reason. Primitives fall through unchanged. Objects are mapped, and each property is processed recursively. Functions get their own branch because a mapped type keeps properties but not call signatures: without the branch, an <code dir=\"ltr\">onSubmit</code> handler would turn into an object you cannot call. Arrays need no special case here, because a homomorphic mapped type applied to an array produces a readonly array.</p>\n<p><code dir=\"ltr\">DeepPartial</code> is the other one you will meet, typically for test fixtures and config overrides:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepPartial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> readonly</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> unknown</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> object</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepPartial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]> }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> checkoutFixture</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">overrides</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepPartial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Checkout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Checkout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">checkoutFixture</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ shipping: { address: { country: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'JO'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } } });</span></span></code></pre></div>\n<p>Here arrays are kept whole on purpose: a partial array of partial line items is rarely what a test means.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Trusting readonly to freeze objects</span></p>\n<p><code dir=\"ltr\">DeepReadonly</code> blocks <code dir=\"ltr\">checkout.placedAt = …</code>, but <code dir=\"ltr\">checkout.placedAt.setFullYear(2020)</code> still compiles, because <code dir=\"ltr\">setFullYear</code> is a method and methods are kept as they are. The same goes for <code dir=\"ltr\">Map#set</code>, <code dir=\"ltr\">Set#add</code> and anything else that mutates through a method. <code dir=\"ltr\">readonly</code> is a promise about property assignment, not immutability. For values that must not change, use <code dir=\"ltr\">ReadonlyMap</code>, <code dir=\"ltr\">ReadonlySet</code> and plain data, and treat <code dir=\"ltr\">Object.freeze</code> as the runtime guarantee.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Paths, and the cost of cleverness</h2>\n<p>Here is the type people reach for when they want <code dir=\"ltr\">get(checkout, 'shipping.address.city')</code> to be checked:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Paths</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">extends</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> object</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}.${</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Paths</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">]></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}[</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> CheckoutPath</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Paths</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;{ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">shipping</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">address</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">city</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">country</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } }; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">totalCents</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }>;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// 'shipping' | 'shipping.address' | 'shipping.address.city' | 'shipping.address.country' | 'totalCents'</span></span></code></pre></div>\n<p>It works, and on a small type it is delightful. It is also a mapped type, a conditional, a template literal and recursion in four lines, and its cost grows with every field and every level. On a real 40-field state object with arrays and dates in it, a type like this can produce thousands of members and make every keystroke in the editor slower.</p>\n<p>TypeScript also has hard limits. Each nested instantiation counts, and after a few dozen levels of ordinary recursion you get <em>Type instantiation is excessively deep and possibly infinite</em>. Since TypeScript 4.5, a conditional type whose recursive call is in <strong>tail position</strong> (the whole result of a branch, as in <code dir=\"ltr\">MyAwaited</code>) is optimised and can run about 1,000 iterations. There is no compiler flag to raise either limit.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>When to stop</span></p>\n<p>On the 400,000-line migration I led, we deleted more clever types than we wrote. The test we used, and the one I recommend, has three questions. <strong>What bug does this type prevent?</strong> Name it, ideally one you have seen in production. <strong>Can a teammate read the error it produces?</strong> If the message is 30 lines of expanded types, people will cast their way out of it, and the protection is gone. <strong>What does it cost?</strong> Check how long the editor takes to show a tooltip, and how the build time changes. A plain <code dir=\"ltr\">string</code> key plus a runtime check, or five typed accessor functions, often gives 90% of the safety for 5% of the complexity.</p>\n</aside>\n<p>Recursive types are the right tool for genuinely recursive data: trees, JSON, nested configs. For everything else, write the boring type first, and reach for the clever one only when a real bug justifies it.</p>\n<p>In the exercise you will write <code dir=\"ltr\">MyAwaited</code>, <code dir=\"ltr\">DeepReadonly</code> and <code dir=\"ltr\">DeepPartial</code>, and use the last one for a test fixture. That closes the type-level section. Section 4 turns to patterns: what experienced teams actually build with all of this.</p>\n","toc":[{"id":"sec-1","text":"Types that refer to themselves","level":2},{"id":"sec-2","text":"Deep transformations","level":2},{"id":"sec-3","text":"Paths, and the cost of cleverness","level":2}],"takeaways":["A type alias can refer to itself, which is how you describe trees, JSON and other nested data.","Recursive type-level functions such as <code dir=\"ltr\">DeepReadonly</code> and <code dir=\"ltr\">DeepPartial</code> need explicit cases for functions and arrays, or they transform things you did not mean to touch.","TypeScript stops recursion after a few dozen nested levels, or about 1,000 for tail-recursive conditional types, with “Type instantiation is excessively deep”.","<code dir=\"ltr\">readonly</code> only blocks property assignment; methods like <code dir=\"ltr\">Date#setFullYear</code> or <code dir=\"ltr\">Map#set</code> still mutate a deeply readonly value.","Before shipping a clever type, ask what bug it prevents, whether a teammate can read its errors, and what it costs the editor."],"further":[{"title":"Recursive conditional types (TypeScript 4.1 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html"},{"title":"Tail-recursion elimination on conditional types (TypeScript 4.5 release notes)","url":"https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html"},{"title":"Awaited utility type","url":"https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype"}],"quiz":[{"q":"<p>With this definition, what is the type of <code dir=\"ltr\">o.lines</code>?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">TypeScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepReadonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#953800;--shiki-dark:#FFA657\">args</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> any</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  ?</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  :</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> extends</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> object</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">readonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> in</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepReadonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#953800;--shiki-dark:#FFA657\">K</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> T</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">declare</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> DeepReadonly</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;{ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">lines</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">sku</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }[] }>;</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">{ sku: string }[]</code>, because mapped types skip arrays","why":"A homomorphic mapped type applied to an array produces an array, and here it also adds <code dir=\"ltr\">readonly</code>."},{"text":"<code dir=\"ltr\">readonly { readonly sku: string }[]</code>","why":"Correct. Mapping over an array type gives a readonly array, and each element is made deeply readonly by the recursive call."},{"text":"<code dir=\"ltr\">{ readonly 0: …; readonly length: number; … }</code>, an object with numeric keys","why":"TypeScript special-cases homomorphic mapped types on arrays and tuples, so the result stays an array type."}],"answer":1},{"q":"<p>Why does a <code dir=\"ltr\">DeepReadonly</code> type usually need a separate branch for functions?</p>","options":[{"text":"Because mapping over a function type turns it into an object without a call signature, so it can no longer be called.","why":"Correct. A mapped type keeps properties, not call signatures, so <code dir=\"ltr\">onSubmit</code> would stop being callable."},{"text":"Because functions cannot be readonly in JavaScript.","why":"The concern is the type transformation, not runtime freezing; a mapped type would strip the call signature."},{"text":"Because <code dir=\"ltr\">keyof</code> of a function type is an error.","why":"<code dir=\"ltr\">keyof</code> of a function type is fine (it is mostly <code dir=\"ltr\">never</code>). The problem is what the mapped type produces."}],"answer":0},{"q":"<p><code dir=\"ltr\">MyAwaited&lt;T&gt; = T extends Promise&lt;infer V&gt; ? MyAwaited&lt;V&gt; : T</code>. What is <code dir=\"ltr\">MyAwaited&lt;Promise&lt;Promise&lt;Order&gt;&gt;&gt;</code>?</p>","options":[{"text":"<code dir=\"ltr\">Promise&lt;Order&gt;</code>","why":"That is one level of unwrapping. The recursive call keeps going while the type is still a promise."},{"text":"<code dir=\"ltr\">Order</code>","why":"Correct. The recursion unwraps one promise per step and stops when the type is no longer a promise."},{"text":"<code dir=\"ltr\">never</code>, because nested promises do not exist at runtime","why":"The type is about what the declaration says; at runtime a resolved promise never contains another promise, which is why unwrapping all levels is right."},{"text":"An error, because a conditional type cannot refer to itself","why":"Recursive conditional types have been allowed since TypeScript 4.1."}],"answer":1},{"q":"<p>A teammate adds a <code dir=\"ltr\">Paths&lt;T&gt;</code> type that generates every dot path of the 40-field checkout state, and the editor becomes sluggish. What is the most pragmatic fix?</p>","options":[{"text":"Raise the compiler’s recursion limit in tsconfig.","why":"There is no tsconfig option for the instantiation depth limit, and slowness is about the amount of work, not the limit."},{"text":"Add more type tests so the type is verified.","why":"Tests check correctness, not cost. The type would be just as slow with tests."},{"text":"Wrap the type in <code dir=\"ltr\">NoInfer</code> so it is not computed at call sites.","why":"<code dir=\"ltr\">NoInfer</code> only affects inference candidates; the type still has to be expanded to check arguments."},{"text":"Replace it with a narrower type, such as typed accessors for the few paths the code actually uses.","why":"Correct. A smaller, explicit type gives the same protection where it matters and costs the compiler almost nothing."}],"answer":3}],"exercise":{"kind":"web","mode":"ts","starter":{"ts":"type Checkout = {\n  id: number;\n  customer: { email: string; country: string };\n  shipping: { method: 'standard' | 'express'; address: { line1: string; city: string } };\n  lines: { sku: string; quantity: number }[];\n  onSubmit: () => void;\n};\n\ntype MyAwaited<T> = T;\ntype DeepReadonly<T> = Readonly<T>;\ntype DeepPartial<T> = Partial<T>;\n\nconst defaultCheckout: Checkout = {\n  id: 1042,\n  customer: { email: 'mona@example.com', country: 'EG' },\n  shipping: { method: 'standard', address: { line1: '12 Nile St', city: 'Cairo' } },\n  lines: [{ sku: 'TENT-2P', quantity: 1 }],\n  onSubmit: () => {},\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction mergeDeep(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n  const out: Record<string, unknown> = { ...base };\n  for (const [key, value] of Object.entries(overrides)) {\n    if (value === undefined) continue;\n    const current = out[key];\n    out[key] = isPlainObject(current) && isPlainObject(value) ? mergeDeep(current, value) : value;\n  }\n  return out;\n}\n\nfunction checkoutFixture(overrides: DeepPartial<Checkout> = {}): Checkout {\n  // mergeDeep works on untyped records; the result has Checkout's shape by construction.\n  return mergeDeep(defaultCheckout, overrides as Record<string, unknown>) as Checkout;\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<MyAwaited<Promise<Promise<Checkout>>>, Checkout>>;\ntype T2 = Expect<Equal<MyAwaited<string>, string>>;\n\ntype Frozen = DeepReadonly<Checkout>;\ntype T3 = Expect<Equal<Frozen['shipping']['address'], { readonly line1: string; readonly city: string }>>;\ntype T4 = Expect<Equal<Frozen['lines'], readonly { readonly sku: string; readonly quantity: number }[]>>;\ntype T5 = Expect<Equal<Frozen['onSubmit'], () => void>>;\n\ntype T6 = Expect<Equal<DeepPartial<Checkout>['shipping'], { method?: 'standard' | 'express'; address?: { line1?: string; city?: string } } | undefined>>;\ntype T7 = Expect<Equal<DeepPartial<Checkout>['lines'], { sku: string; quantity: number }[] | undefined>>;\n\nfunction mutationTest(frozen: Frozen) {\n  // @ts-expect-error: nested properties are readonly\n  frozen.shipping.address.city = 'Amman';\n}\n\nconst fixture = checkoutFixture({ shipping: { address: { city: 'Amman' } }, customer: { country: 'JO' } });\nconsole.log(fixture.shipping.address, fixture.customer);\n"},"solution":{"ts":"type Checkout = {\n  id: number;\n  customer: { email: string; country: string };\n  shipping: { method: 'standard' | 'express'; address: { line1: string; city: string } };\n  lines: { sku: string; quantity: number }[];\n  onSubmit: () => void;\n};\n\ntype MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T;\n\ntype DeepReadonly<T> = T extends (...args: any[]) => any\n  ? T\n  : T extends object\n    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n    : T;\n\ntype DeepPartial<T> = T extends readonly unknown[]\n  ? T\n  : T extends object\n    ? { [K in keyof T]?: DeepPartial<T[K]> }\n    : T;\n\nconst defaultCheckout: Checkout = {\n  id: 1042,\n  customer: { email: 'mona@example.com', country: 'EG' },\n  shipping: { method: 'standard', address: { line1: '12 Nile St', city: 'Cairo' } },\n  lines: [{ sku: 'TENT-2P', quantity: 1 }],\n  onSubmit: () => {},\n};\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction mergeDeep(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {\n  const out: Record<string, unknown> = { ...base };\n  for (const [key, value] of Object.entries(overrides)) {\n    if (value === undefined) continue;\n    const current = out[key];\n    out[key] = isPlainObject(current) && isPlainObject(value) ? mergeDeep(current, value) : value;\n  }\n  return out;\n}\n\nfunction checkoutFixture(overrides: DeepPartial<Checkout> = {}): Checkout {\n  // mergeDeep works on untyped records; the result has Checkout's shape by construction.\n  return mergeDeep(defaultCheckout, overrides as Record<string, unknown>) as Checkout;\n}\n\n// ---- Type tests (do not edit) ----\ntype T1 = Expect<Equal<MyAwaited<Promise<Promise<Checkout>>>, Checkout>>;\ntype T2 = Expect<Equal<MyAwaited<string>, string>>;\n\ntype Frozen = DeepReadonly<Checkout>;\ntype T3 = Expect<Equal<Frozen['shipping']['address'], { readonly line1: string; readonly city: string }>>;\ntype T4 = Expect<Equal<Frozen['lines'], readonly { readonly sku: string; readonly quantity: number }[]>>;\ntype T5 = Expect<Equal<Frozen['onSubmit'], () => void>>;\n\ntype T6 = Expect<Equal<DeepPartial<Checkout>['shipping'], { method?: 'standard' | 'express'; address?: { line1?: string; city?: string } } | undefined>>;\ntype T7 = Expect<Equal<DeepPartial<Checkout>['lines'], { sku: string; quantity: number }[] | undefined>>;\n\nfunction mutationTest(frozen: Frozen) {\n  // @ts-expect-error: nested properties are readonly\n  frozen.shipping.address.city = 'Amman';\n}\n\nconst fixture = checkoutFixture({ shipping: { address: { city: 'Amman' } }, customer: { country: 'JO' } });\nconsole.log(fixture.shipping.address, fixture.customer);\n"},"checks":[{"id":"types","typecheck":true},{"id":"fixture","test":"const f = checkoutFixture({ shipping: { address: { city: 'Amman' } } }); return f.shipping.address.city === 'Amman' && f.shipping.address.line1 === '12 Nile St' && f.shipping.method === 'standard' && f.lines.length === 1;"}],"title":"Go deep, carefully","prompt":"<p>Implement three recursive types (do not use the built-in <code dir=\"ltr\">Awaited</code>):</p>\n<ul>\n<li><code dir=\"ltr\">MyAwaited&lt;T&gt;</code>: unwrap nested promises until the type is not a promise.</li>\n<li><code dir=\"ltr\">DeepReadonly&lt;T&gt;</code>: make every property at every level <code dir=\"ltr\">readonly</code>, turn arrays into readonly arrays of deeply readonly elements, and leave functions untouched (still callable).</li>\n<li><code dir=\"ltr\">DeepPartial&lt;T&gt;</code>: make every property at every level optional, but keep arrays as they are (a fixture replaces a whole array).</li>\n</ul>\n<p><code dir=\"ltr\">checkoutFixture</code> already merges overrides at runtime; once <code dir=\"ltr\">DeepPartial</code> is right, the nested override in the example compiles.</p>\n","hints":["Each type is a conditional that calls itself in the branch that goes one level deeper.","Order the cases: functions first (return <code dir=\"ltr\">T</code>), then objects (map and recurse), then everything else (return <code dir=\"ltr\">T</code>). Arrays are objects, and a homomorphic mapped type keeps them arrays.","For <code dir=\"ltr\">DeepPartial</code>, add a first case <code dir=\"ltr\">T extends readonly unknown[] ? T : …</code> so arrays are not made partial."],"explanation":"<p><code dir=\"ltr\">MyAwaited</code> recurses in tail position, peeling one promise per step. <code dir=\"ltr\">DeepReadonly</code> needs a function branch because a mapped type keeps properties but drops call signatures, so <code dir=\"ltr\">onSubmit</code> would otherwise stop being callable; arrays need no extra case because mapping over an array type yields a readonly array. <code dir=\"ltr\">DeepPartial</code> deliberately stops at arrays, which matches how <code dir=\"ltr\">mergeDeep</code> treats them: an override replaces the whole array. The casts in <code dir=\"ltr\">checkoutFixture</code> are local and commented: the merge works on untyped records, and the fixture’s type is restored once at the boundary.</p>\n","checkLabels":{"types":"The file type-checks with zero errors, including all type tests","fixture":"<code dir=\"ltr\">checkoutFixture</code> merges nested overrides into the defaults"}},"words":728,"readMinutes":3}