Query responses
action=query is the most heavily used action, and its response is typed by composition: module results merge into shared interfaces, keyed by where the server puts them.
The two landing spots
query.pages[]: the union of the page-levelprop=modules. Each entry is anApiPagethat merges the fields of every coveredprop=module. Which fields a page actually carries depends on theprop=you passed; the type is their union for convenience, yet wider than any single request, so fields you never asked for look available.- The
queryroot:list=/meta=results, plus the occasionalprop=whose result is not page-scoped (e.g.prop=stashimageinfolands atquery.stashimageinfo). They merge directly intoApiQueryResult:query.recentchangesforlist=recentchanges,query.generalformeta=siteinfo, and so on. They do not go through the pages projection below.
Continuation
Modules that can return more results than one request carries add their tokens to a top-level continue object, typed as ApiQueryContinue — another declaration-merged interface, where each module contributes a key named after its parameter prefix (e.g. cmcontinue for list=categorymembers), next to the generic opaque continue token. While the object is present, pass its keys back on the next request; spreading the whole object into the request is the usual way. Once it is absent, you have everything:
Both landing spots take part in continuation: list results and generator-driven page queries each contribute their own keys.
Log event details
Three modules surface the structured details of log entries, and the server normalizes them all into one shape:
list=logevents: each log entry'sparams(leprop=details)list=recentchanges:logparamsontype=logentries (rcprop=loginfo)list=watchlist:logparamsontype=logentries (wlprop=loginfo)
All three are typed as ApiLogEventParams, covering every log action MediaWiki core writes.
Which keys an entry actually carries depends on its type/action (a block/block entry carries duration/flags/sitewide, a move/move entry carries target_ns/target_title; each key's doc names the actions that carry it). Not every log action has detail keys: on delete/delete and create/create entries, params is an empty object {} — present, just empty.
As with ApiPage, the union is wider than any single entry, and the compiler cannot correlate params with the entry's action, so check type/action at runtime before reading action-specific keys:
Keys outside the declared set fall through to the index signature and read as unknown instead of erroring. To promote keys you actually receive — say, from a site-custom log action — merge them into the interface:
Merged, event.params?.foo reads as string | undefined instead of unknown. An augmented key has the same standing as a built-in one: it joins the shape shared by all log entries, so reading it compiles on entries of any action.
Which action a field belongs to is a docstring convention (built-in keys work the same way: duration only ever appears on block-family rows); at runtime, go by type/action. If you want compile-time restriction of a read to one action, skip the augmentation and narrow params to a local shape (ApiLogEventParams & { foo?: string }) at the call site.
The same seam for query fields is described in Opt-in extension packs.
Narrowing pages with QueryPage
ApiPage is a union, and the compiler does not know which prop= you requested, so a field you never asked for still typechecks. QueryPage<K> puts that information back, narrowing the page to the props you actually requested:
K takes field keys of ApiPage. For most prop= modules the key matches the module name, and passing a union type projects several at once, e.g. QueryPage<"revisions" | "categories">.
A few modules contribute several page-level keys at once; naming every key is long and easy to get wrong, so you can use the ready-made per-module aliases instead:
Spelling the keys by hand has two traps:
QueryPage<"info">does not compile, becauseprop=infohas noinfokey.QueryPage<"imageinfo">projects only theimageinfoarray, not its sibling fields (imagerepository/badfile).
How the projection works
QueryPage is built from ApiPageIdentity (the identity fields shared by every page), the index a generator=search / generator=prefixsearch request injects, plus Pick<ApiPage, K>. It is a pure type-level operation; it does not interfere with the per-module declaration merging, and fields contributed by opt-in extension packs participate in the projection too.
QueryPageExisting (and InfoPageExisting) additionally require the picked fields their module writes unconditionally: the identity fields plus the prop=info core set and revisions (see PropConstantKeys). Gated fields stay optional.