Repository navigation
API: coarse-grained checker queries for tools that extract a whole API surface (getSymbolDocumentations, getMembersOfType)Β #65051
Description
π Search Terms
typescript/sync api round trips, getPropertiesOfType per member, bulk symbol documentation, getMembersOfType, api extractor performance, jsii typescript 7, out-of-process checker queries
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β Suggestion
I am an independent contributor experimenting with running jsii (the AWS CDK's multi-language binding generator) on the TypeScript 7 programmatic API. This proposal comes out of that work, and I think it applies to other tools of the same kind.
Tools that extract an API surface from a whole program (jsii, TypeDoc, API Extractor, documentation generators) visit every exported type and, for every member, ask the checker for the member's properties, its declared type, the type that declares it, and its JSDoc. On the out-of-process API each of those questions is one round trip, so the traversal is bounded by the number of round trips rather than by checker work. jsii spends about 33 s traversing aws-cdk-lib and issues about 700k requests in that time; the server spends only about 6 s of it computing types, and the rest goes to waiting for round trips and handling messages.
I propose two additional methods. One returns the JSDoc of many symbols at once. The other returns, for a single type, everything such a traversal needs about its members (symbol, declaring type, kind, JSDoc, declared type) in one round trip. On aws-cdk-lib this brings the request count from 700k down to 300k and the traversal from 33 s to 28 s. Existing methods are unchanged and the new ones return the same handles, so callers can migrate incrementally.
Both methods run against the existing API checker and return the same symbol and type handles the fine-grained methods return; callers rely on handle identity to correlate types across calls, so that is preserved.
getSymbolDocumentations(symbols[]) returns, per symbol, exactly what getJsDocTags and getDocumentationComment return. One request per type's member list instead of two per member.
getMembersOfType(type, filter) returns the members of a type that satisfy the filter. Each entry carries the member's symbol, the symbol of the type that declares it, its kind (method or constructor parameter property), its JSDoc, and on request its declared type. This is the union of what a member-processing loop asks for individually today.
The filter exists because of a measurement. A first attempt that fetched the declared type of every member and discarded the unwanted ones afterwards was slower than the fine-grained traversal: at aws-cdk-lib scale, private and internal members are the majority, and the cost of materialising types for members the caller throws away cancelled the round-trip savings. To materialise only the types of members that will actually be used, the caller has to tell the server what to drop.
The filter is an options object of independent conditions:
| option | meaning | jsii passes | other tools |
|---|---|---|---|
visibility |
visibilities to include; ECMAScript private names count as private | public, protected | a public-API extractor passes public only |
kinds |
member kinds to include (property / accessor / method / parameterProperty) | all | a documentation tool that renders accessors separately |
excludeJsDocTags |
drop members whose JSDoc carries any of these tags | internal (plus deprecated when stripping) | TypeDoc's @hidden, API Extractor's @internal / @alpha |
excludeNamePattern (RE2) |
drop members whose name matches | ^_ |
any naming convention |
includeInheritedFrom |
which declaring types contribute members | the type's unexported base types | all for a flattened view, empty for own members only |
includeDeclaredType |
attach the declared type to each member | true | false for a name-only listing |
π Motivating Example
Request mix for jsii's TypeScript 7 backend over aws-cdk-lib (the snapshot jsii-compiler uses for its benchmark, 20,221 types and 97,071 members): getJsDocTags 134k, getTypeAtLocation 125k, getDocumentationComment 117k, getTypeOfSymbolAtLocation 105k, getTypeFromTypeNode 47k, getPropertiesOfType 23k.
A Go execution trace of the server shows the handler for these per-member methods takes 1.6β5 Β΅s each; the round trip the client observes is 26 Β΅s, and the server spends 24 s of a 45 s run blocked waiting for the next request. The traversal is dominated by the number of messages, not by the checker.
The existing mechanisms do not help this pattern. batchRequests cannot bundle a member traversal because its requests depend on each other: the type's property list is needed before the properties' types can be asked for. Client-side caching does not help either: the duplicate rate for documentation reads and getPropertiesOfType is 0%, and memoising getTypeAtLocation (80% duplicates) removed 113k requests but no measurable wall clock, because repeated requests are cheap on the server and the cost is the message itself.
Results, same fork build, c7i.4xlarge, aws-cdk-lib benchmark, with the sync-channel waiting improvement from the separate proposal enabled, median of 3 runs, .jsii output byte-identical between the two paths:
| path | wall | requests | transport wait | bytes sent |
|---|---|---|---|---|
| fine-grained (today's methods) | 34.5 s | 698k | 8.6 s | 131 MB |
getSymbolDocumentations + getMembersOfType |
29.4 s | 304k | 5.4 s | 52 MB |
A current aws-cdk-lib checkout built from source (2 runs): 43.2 / 43.1 s β 37.8 / 38.2 s, requests 783k β 340k, bytes sent 168 MB β 65 MB, server time unchanged (22.1 β 21.5 s).
The implementation, with Go unit tests, is on the fork branch WinterYukky/TypeScript api/coarse-endpoints-generic-filter-1792824451.
π» Use Cases
- Tools that extract an API surface from a whole program through the API: jsii (multi-language bindings for the AWS CDK), documentation generators such as TypeDoc, and API extractors.
- Each member costs several round trips today, and neither
batchRequests(the requests depend on each other) nor client-side caching (few duplicates) reduces them. - None; jsii issues the per-member requests and accepts the round-trip cost.
Activity
aetherxeg-source commentedon Oct 30, 2026
Thanks for the detailed performance measurements and the proposed coarse-grained checker APIs.
One correctness boundary seems particularly important for
getMembersOfType: preserving the exact API surface produced by the existing fine-grained traversal, especially when server-side filtering happens before declared types are materialized.Would it be useful to add a differential regression fixture that runs both API paths against the same program and compares their normalized member results?
A focused fixture could cover:
The comparison could check symbol/type handle identity, declared types, documentation, visibility, and deterministic member ordering.
The byte-identical
.jsiibenchmark is useful end-to-end evidence; a small semantic-equivalence regression would complement it by making API-surface drift easier to isolate if the implementation evolves.This is a proposal based on reviewing the published issue and its validation details; I haven't executed the prototype or its tests.
WinterYukky commentedon Oct 30, 2026
Thanks, good suggestion. I'll add a differential fixture comparing both paths on the same program to the PR.