Skip to content

Commit

Permalink
Support "equivalent to" for values (#252)
Browse files Browse the repository at this point in the history
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 microsoft/TypeScript#49852
  • Loading branch information
danvk committed Mar 10, 2024
1 parent f7bf236 commit 944d48d
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 12 deletions.
32 changes: 25 additions & 7 deletions src/code-sample.ts
Expand Up @@ -300,27 +300,45 @@ 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;
if (!content.includes('equivalent to')) {
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,
Expand Down
118 changes: 113 additions & 5 deletions src/test/__snapshots__/asciidoc.test.ts.snap
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
],
}
`;
Expand Down Expand Up @@ -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": {},
},
]
`;
Expand Down
35 changes: 35 additions & 0 deletions src/test/code-sample.test.ts
Expand Up @@ -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"
`);
});
});
24 changes: 24 additions & 0 deletions src/test/inputs/equivalent.asciidoc
Expand Up @@ -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")
}
}
----

0 comments on commit 944d48d

Please sign in to comment.