[docs-infra] Render preserved keyof type operators - #1817
Conversation
typescript-api-extractor 1.0.0-beta.6 stopped expanding authored `keyof` into a literal union and instead emits `typeOperator` and `typeQuery` nodes. formatType dispatches on `kind` and ends in a bare `return 'unknown'`, so every `keyof` type documented as the literal word `unknown`. In Base UI this collapsed `Field.Error`'s `match` to `boolean | unknown` (which TypeScript reduces to `unknown`, so the table claimed any value was accepted) and `useRender`'s `defaultTagName` to `unknown`. The operator node still carries the checker result in `resolvedType`, so format that and keep the previous output. No length-dependent behaviour: sizing the output to the type would be inconsistent with the rest of the formatter.
Deploy previewBundle sizeTotal Size Change: 0B(0.00%) - Total Gzip Change: 0B(0.00%) Show details for 71 more bundles@mui/internal-docs-infra/abstractCreateDemo parsed: 0B(0.00%) gzip: 0B(0.00%) PerformanceTotal duration: 16.96 ms -2.75 ms(-13.9%) | Renders: 5 (+0) | Paint: 58.71 ms -13.83 ms(-19.1%)
5 tests within noise — details Metric alarms
Check out the code infra dashboard for more information about this PR. |
Hand-built nodes assert whatever shape the test author imagined, so they cannot catch typescript-api-extractor changing its output — the exact class of break they were added for. Extract the in-memory parser harness from transformConstantGroup.test into parseTestSources and drive these cases from real source instead. The helper gains a virtual default library, which is what makes the parser preserve `keyof`: an operand it can expand, such as a locally declared interface, never reaches the formatter as an operator. Also drop the named-alias branch. Both TypeOperatorNode construction sites in the parser pass `undefined` for the type name, so the branch was unreachable and its test asserted behaviour the parser does not produce. The alias name arrives on the resolved union instead.
|
@claude review |
This comment was marked as resolved.
This comment was marked as resolved.
Keep a type parameter operand by name in raw declarations. The checker resolves `keyof T` to its base constraint, which holds no type parameter to recover `T` from, so a documented `Box<T>` rendered `string | number | symbol` with the declared `T` left unused. Concrete operands still resolve, so `keyof React.JSX.IntrinsicElements` keeps documenting its key set rather than collapsing to the authored syntax. Share the extraction policy through constants.ts. The test harness only passed `includeExternalTypes`, while the pipeline also caps depth and property count — and those caps are what decide whether an operand is resolved at all, so a `keyof` over a wide interface could have resolved in tests and stayed unresolved in the real build. Rename the harness to parseSources.testUtils.ts, matching coordinatePreference.testUtils.ts. The package ships raw TypeScript from src, so a fixture builder sitting next to processTypes.ts under a `parse` purpose prefix reads as a supported entry point. Declarations for testUtils siblings are excluded from the build; the transpiled output still ships because that ignore list lives in code-infra.
Fail the build on an unhandled node kind. `formatType` ended in a silent `return 'unknown'`, which is how preserved `keyof` operators shipped as the literal word `unknown` through a parser upgrade. The kinds that legitimately fall through are the export-level ones, so naming them leaves a `never` assertion that turns the next parser addition into a type error instead of a quietly wrong props table. Use PARSER_OPTIONS at its call sites rather than re-aliasing it, and switch the last hand-rolled copy of the extraction policy over to it. Revert the testUtils rename and its build exclusion. How shared test helpers should be named and kept out of the published package is its own question, not one to settle inside a regression fix.
`formatExternalTypeDefinition` is a second kind dispatcher with its own silent `unknown` tail, so the External Types section did not inherit the formatType fix. Two regressions against the pre-upgrade output. An external union with a `keyof` member failed the all-members-are-literals gate and was dropped from the section entirely, leaving the props table naming a type whose definition appeared nowhere on the page. An external function type with a `keyof` parameter documented as `(tone: unknown) => string`. Resolve operator members before both the gate and the formatting, so these read as the keys they stand for. Flattening ahead of the existing `uniq` also means a key already listed alongside the operator is no longer repeated.
`'size' | keyof Config` documented as `'size' | 'size' | 'color'`. The operator resolved to its keys as one finished string, after the union had already flattened, ordered and deduped its members, so those keys never took part. Flatten them alongside the other members instead. Whether an operator expands at all is now asked in one place, so the union branch cannot disagree with the operator branch and start flattening a `keyof T` that is being kept by name.
`&` binds tighter than `|`, so a union member joined into an intersection
loses its grouping: `keyof Config & Meta` documented as `'size' | 'color'
& Meta`, which reads as `'size' | ('color' & Meta)`.
Wrap a member that reads as a union at its top level. Pipes nested inside
a member are already grouped by their own brackets and are left alone, so
`{ x: string | number } & Meta` is untouched. Intersections inside a union
need nothing, since those already bind the way they read.
This is not specific to preserved operators: in Base UI it corrects
`Item[] & RejectGroupShapedItems<Item>`, whose conditional type resolves
to a union.
Group a composite operand in the authored-syntax fallback. `keyof (A | B)` rendered as `keyof A | B`, which reads as `(keyof A) | B` — the same precedence bug the intersection join had, at a site this branch added. Move the grouping rule into its own module so both formatters apply it rather than each spelling it at a call site, and generalize it to the operator being bound against. Use the package's `satisfies` idiom for the exhaustiveness assert. It names the missing kind in the error instead of just reporting `never`, and leaves the fallback returning a string rather than a node. Drive the remaining fixtures from source. The hand-built external nodes claimed a shape the parser never produces here, since this section always expands.
typescript-api-extractor
1.0.0-beta.6(#1804, shipped in canary.34) stopped expanding authoredkeyofand now emitstypeOperator/typeQuerynodes. The formatters dispatch onkindand fell through to a bareunknown, so everykeyoftype documented as the literal wordunknown— in Base UI,Field.Error'smatchbecameboolean | unknown(which TypeScript reduces tounknown, so the table claimed any value was accepted) anduseRender'sdefaultTagNamelost its 178-member union.The node still carries the checker result, so this formats that and keeps the previous output. A type parameter operand stays as
keyof Tin raw declarations, since its resolved base constraint holds noTto recover.Also here, found while fixing the above:
formatExternalTypeDefinitionhad the same fallthrough, and its all-members-are-literals gate silently dropped external unions containing akeyoffrom the External Types section entirely.keyofmember of a union skipped dedup, so'size' | keyof Configdocumented as'size' | 'size' | 'color'.A | B & Cread asA | (B & C). Pre-existing and notkeyof-specific — it corrects Base UI'scomboboxdataparam today.pnpm typescriptinstead of documenting itself asunknown.Verified against Base UI at upstream master: mui/base-ui#5580.