Repository: danvk/literate-ts (ts)

Issue:
Inferred type equivalent to
#145 added support for "equivalent to" assertions for type aliases. But what about values?

```ts
interface ABC { a: string; b: string; c: string; }
function foo(abc: ABC) {
  let k: keyof ABC;
  for (k in abc) {
    // ^? let k: keyof ABC (equivalent to "a" | "b" | "c")
  }
}
```

Pull request description:
Support "equivalent to" for values
Fixes #167 

Weirdly the `Resolve` helper only resolves types at the top level, not in functions:

```ts
interface Point {
  x: number;
  y: number;
}

type Resolve<Raw> = Raw extends Function ? Raw : { [K in keyof Raw]: Raw[K] };
function foo(pt: Point) {
  let k: keyof Point;
  for (k in pt) {
    // ^? let k: keyof Point
    type SynthK = Resolve<keyof Point>;
    //   ^? type SynthK = keyof Point
  }
}

type SynthT2 = Resolve<keyof Point>;
//   ^? type SynthT2 = "x" | "y"`);
```

This might be https://github.com/microsoft/TypeScript/issues/49852

Patch:
diff --git a/src/code-sample.ts b/src/code-sample.ts
index f09b19c..54e2984 100644
--- a/src/code-sample.ts
+++ b/src/code-sample.ts
@@ -300,6 +300,13 @@ const EQUIVALENT_RE = /\^\? type ([A-Za-z0-9_]+) = (.*)( \(equivalent to (.*)\))
 const EQUIVALENT_MULTILINE_RE =
   /\^\? type ([A-Za-z0-9_]+) = (.*)(\n\s*\/\/ +\(equivalent to (.*)\))$/m;
 
+const VALUE_EQUIVALENT_RE = /\^\? [^ ]+ ([A-Za-z0-9_]+): (.*)( \(equivalent to (.*)\))$/m;
+const VALUE_EQUIVALENT_MULTILINE_RE =
+  /\^\? [^ ]+ ([A-Za-z0-9_]+): (.*)(\n\s*\/\/ +\(equivalent to (.*)\))$/m;
+
+const RESOLVE_HELPER =
+  '\ntype Resolve<Raw> = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};';
+
 /** Patch the code sample to test "equivalent to" types */
 export function addResolvedChecks(sample: CodeSample): CodeSample {
   const {content} = sample;
@@ -307,20 +314,31 @@ export function addResolvedChecks(sample: CodeSample): CodeSample {
     return sample;
   }
 
+  let synthName, type, equivClause, equivType;
   const m = EQUIVALENT_RE.exec(content) || EQUIVALENT_MULTILINE_RE.exec(content);
-  if (!m) {
-    return sample;
+  if (m) {
+    [, type, , equivClause, equivType] = m;
+    synthName = `Synth${type}`;
+  } else {
+    const mv = VALUE_EQUIVALENT_RE.exec(content) || VALUE_EQUIVALENT_MULTILINE_RE.exec(content);
+    if (mv) {
+      let varName;
+      [, varName, type, equivClause, equivType] = mv;
+      synthName = 'Synth' + varName.charAt(0).toUpperCase() + varName.slice(1);
+    } else {
+      return sample;
+    }
   }
 
-  const [, typeName, _raw, equivClause, equivType] = m;
-
   // Strip the "equivalent to" bit, add Resolve<T> helper and secondary type assertion.
   // See https://github.com/danvk/literate-ts/issues/132 and
   // https://effectivetypescript.com/2022/02/25/gentips-4-display/
+  // Resolve is only able to "resolve" types at the top level; it can't be inserted in-place.
+  // So `type` could refer to something out of scope, but hopefully that doesn't happen.
   let newContent = content.replace(equivClause, '');
-  newContent += '\ntype Resolve<Raw> = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};';
-  newContent += `\ntype Synth${typeName} = Resolve<${typeName}>;`;
-  newContent += `\n//   ^? type Synth${typeName} = ${equivType}\n`;
+  newContent += RESOLVE_HELPER;
+  newContent += `\ntype ${synthName} = Resolve<${type}>;`;
+  newContent += `\n//   ^? type ${synthName} = ${equivType}\n`;
 
   return {
     ...sample,


Test patch:
diff --git a/src/test/__snapshots__/asciidoc.test.ts.snap b/src/test/__snapshots__/asciidoc.test.ts.snap
index e41b7a9..b2c224c 100644
--- a/src/test/__snapshots__/asciidoc.test.ts.snap
+++ b/src/test/__snapshots__/asciidoc.test.ts.snap
@@ -105,7 +105,7 @@ exports[`checker asciidoc checker snapshots "./src/test/inputs/equivalent.asciid
   "logs": [
     "---- BEGIN FILE ./src/test/inputs/equivalent.asciidoc
 ",
-    "Found 4 code samples in ./src/test/inputs/equivalent.asciidoc",
+    "Found 6 code samples in ./src/test/inputs/equivalent.asciidoc",
     "BEGIN #././src/test/inputs/equivalent.asciidoc:8 (--filter equivalent-8)
 ",
     "Code passed type checker.",
@@ -191,16 +191,68 @@ type SynthT2 = Resolve<T2>;
     "tsconfig options: {"strictNullChecks":true,"module":1,"esModuleInterop":true}",
     "
 END #././src/test/inputs/equivalent.asciidoc:39 (--- ms)
+",
+    "BEGIN #././src/test/inputs/equivalent.asciidoc:48 (--filter equivalent-48)
+",
+    "Code passed type checker.",
+    "Twoslash type assertion match:",
+    "  Expected: type T = keyof Point",
+    "    Actual: type T = keyof Point",
+    "Twoslash type assertion match:",
+    "  Expected: let k: keyof Point",
+    "    Actual: let k: keyof Point",
+    "Twoslash type assertion match:",
+    "  Expected: type SynthK = "x" | "y"",
+    "    Actual: type SynthK = "x" | "y"",
+    "  3/3 twoslash type assertions matched.",
+    "
+END #././src/test/inputs/equivalent.asciidoc:48 (--- ms)
+",
+    "BEGIN #././src/test/inputs/equivalent.asciidoc:60 (--filter equivalent-60)
+",
+    "Code passed type checker.",
+    "Twoslash type assertion match:",
+    "  Expected: type T = keyof Point",
+    "    Actual: type T = keyof Point",
+    "Twoslash type assertion match:",
+    "  Expected: let k: keyof Point",
+    "    Actual: let k: keyof Point",
+    "💥 ./src/test/inputs/equivalent.asciidoc:67:6-12: Failed type assertion for \`SynthK\`
+  Expected: type SynthK = "x" | "y" | "z"
+    Actual: type SynthK = "x" | "y"",
+    "  2/3 twoslash type assertions matched.",
+    "interface Point {
+  x: number;
+  y: number;
+}
+
+type T = keyof Point;
+//   ^? type T = keyof Point
+function foo(pt: Point) {
+  let k: keyof Point;
+  for (k in pt) {
+    // ^? let k: keyof Point
+  }
+}
+type Resolve<Raw> = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};
+type SynthK = Resolve<keyof Point>;
+//   ^? type SynthK = "x" | "y" | "z"
+",
+    "tsconfig options: {"strictNullChecks":true,"module":1,"esModuleInterop":true}",
+    "
+END #././src/test/inputs/equivalent.asciidoc:60 (--- ms)
 ",
     "---- END FILE ./src/test/inputs/equivalent.asciidoc
 ",
   ],
   "statuses": [
     "1/1: ././src/test/inputs/equivalent.asciidoc",
-    "1/1: ././src/test/inputs/equivalent.asciidoc: 1/4 ././src/test/inputs/equivalent.asciidoc:8",
-    "1/1: ././src/test/inputs/equivalent.asciidoc: 2/4 ././src/test/inputs/equivalent.asciidoc:23",
-    "1/1: ././src/test/inputs/equivalent.asciidoc: 3/4 ././src/test/inputs/equivalent.asciidoc:31",
-    "1/1: ././src/test/inputs/equivalent.asciidoc: 4/4 ././src/test/inputs/equivalent.asciidoc:39",
+    "1/1: ././src/test/inputs/equivalent.asciidoc: 1/6 ././src/test/inputs/equivalent.asciidoc:8",
+    "1/1: ././src/test/inputs/equivalent.asciidoc: 2/6 ././src/test/inputs/equivalent.asciidoc:23",
+    "1/1: ././src/test/inputs/equivalent.asciidoc: 3/6 ././src/test/inputs/equivalent.asciidoc:31",
+    "1/1: ././src/test/inputs/equivalent.asciidoc: 4/6 ././src/test/inputs/equivalent.asciidoc:39",
+    "1/1: ././src/test/inputs/equivalent.asciidoc: 5/6 ././src/test/inputs/equivalent.asciidoc:48",
+    "1/1: ././src/test/inputs/equivalent.asciidoc: 6/6 ././src/test/inputs/equivalent.asciidoc:60",
   ],
 }
 `;
@@ -1013,6 +1065,62 @@ type T = keyof Point;
     "targetFilename": null,
     "tsOptions": {},
   },
+  {
+    "auxiliaryFiles": [],
+    "checkJS": false,
+    "content": "function foo(pt: Point) {
+  let k: keyof Point;
+  for (k in pt) {
+    // ^? let k: keyof Point (equivalent to "x" | "y")
+  }
+}",
+    "descriptor": "./equivalent.asciidoc:48",
+    "id": "equivalent-48",
+    "isTSX": false,
+    "language": "ts",
+    "lineNumber": 47,
+    "nodeModules": [],
+    "prefixes": [
+      {
+        "id": "equivalent-8",
+      },
+    ],
+    "prefixesLength": 0,
+    "replacementId": undefined,
+    "sectionHeader": null,
+    "skip": false,
+    "sourceFile": "equivalent.asciidoc",
+    "targetFilename": null,
+    "tsOptions": {},
+  },
+  {
+    "auxiliaryFiles": [],
+    "checkJS": false,
+    "content": "function foo(pt: Point) {
+  let k: keyof Point;
+  for (k in pt) {
+    // ^? let k: keyof Point (equivalent to "x" | "y" | "z")
+  }
+}",
+    "descriptor": "./equivalent.asciidoc:60",
+    "id": "equivalent-60",
+    "isTSX": false,
+    "language": "ts",
+    "lineNumber": 59,
+    "nodeModules": [],
+    "prefixes": [
+      {
+        "id": "equivalent-8",
+      },
+    ],
+    "prefixesLength": 0,
+    "replacementId": undefined,
+    "sectionHeader": null,
+    "skip": false,
+    "sourceFile": "equivalent.asciidoc",
+    "targetFilename": null,
+    "tsOptions": {},
+  },
 ]
 `;
 
diff --git a/src/test/code-sample.test.ts b/src/test/code-sample.test.ts
index bece97f..5da3b8c 100644
--- a/src/test/code-sample.test.ts
+++ b/src/test/code-sample.test.ts
@@ -555,4 +555,39 @@ describe('addResolvedChecks', () => {
       //   ^? type SynthT2 = "x" | "y"
       `);
   });
+
+  it('should patch a type assertion on a value', () => {
+    const sample = applyPrefixes(
+      extractSamples(
+        dedent`
+          You can use the same pattern with values as well as types:
+
+          [source,ts]
+          ----
+          function foo(pt: Point) {
+            let k: keyof Point;
+            for (k in pt) {
+              // ^? let k: keyof Point (equivalent to "x" | "y")
+            }
+          }
+          ----
+        `,
+        'equivalent-assertion',
+        'source.asciidoc',
+      ),
+    );
+
+    const actual = addResolvedChecks(sample[0]).content;
+    expect(actual).toEqual(dedent`
+      function foo(pt: Point) {
+        let k: keyof Point;
+        for (k in pt) {
+          // ^? let k: keyof Point
+        }
+      }
+      type Resolve<Raw> = Raw extends Function ? Raw : {[K in keyof Raw]: Raw[K]};
+      type SynthK = Resolve<keyof Point>;
+      //   ^? type SynthK = "x" | "y"
+      `);
+  });
 });
diff --git a/src/test/inputs/equivalent.asciidoc b/src/test/inputs/equivalent.asciidoc
index 0827efd..69abfb4 100644
--- a/src/test/inputs/equivalent.asciidoc
+++ b/src/test/inputs/equivalent.asciidoc
@@ -40,3 +40,27 @@ type T2 = keyof Point;
 //   ^? type T2 = keyof Point
 //      (equivalent to "x" | "y" | "z")
 ----
+
+You can use the same pattern with values as well as types:
+
+[source,ts]
+----
+function foo(pt: Point) {
+  let k: keyof Point;
+  for (k in pt) {
+    // ^? let k: keyof Point (equivalent to "x" | "y")
+  }
+}
+----
+
+This one should fail:
+
+[source,ts]
+----
+function foo(pt: Point) {
+  let k: keyof Point;
+  for (k in pt) {
+    // ^? let k: keyof Point (equivalent to "x" | "y" | "z")
+  }
+}
+----