SvelteKit universal load union type inference error with optional properties in one branch
Versions: typescript 5.8.2, @sveltejs/kit 2.69.1, svelte 4.2.19, svelte-check 4.1.5.
A SvelteKit universal load returned object literals from two branches with an IDENTICAL set of keys — the non-200 branch supplied a hand-written fallback for one of them:
export const load: PageLoad = async (event) => {
const res = await getFeed({ days: 7 }, load_opts(event));
if (res.status !== 200) {
return { feed: { items: [] }, user, title }; // fallback literal
}
return { feed: res.data, user, title }; // feed: FeedResponse
};Pages had accessed data.feed.week_start for months with zero type errors. I then added two optional properties to the SUCCESS branch only (description and image, both string | undefined). svelte-check immediately failed — but on a property I had not touched, in a component I had not edited:
Error: Property 'week_start' does not exist on type 'FeedResponse | { items: never[]; }'.
Property 'week_start' does not exist on type '{ items: never[]; }'. (ts)Assumed the error was pre-existing and unrelated, since nothing I changed went near week_start. Stashed only my edits and re-ran svelte-check: 0 errors. So my two-property addition caused it. Could not see the connection — the fallback literal was untouched, the failing property belongs to the other branch's type, and adding optional fields to one branch should be additive. Adding ?? null guards at each use site suppressed it but felt like whack-a-mole against something I did not understand.
Root cause: TypeScript's union subtype reduction was silently masking a heterogeneous union, and adding a property to one branch unmasked it.
When a function returns object literals from several branches, TypeScript infers a union of those types, then reduces it by discarding any constituent that is a subtype of another.
Before the change the two constituents were:
A = { feed: FeedResponse; user: string; title: string }
B = { feed: { items: never[] }; user: string; title: string }FeedResponse has all-optional properties (a generated OpenAPI response type), so { items: never[] } is assignable to it — never[] is assignable to Item[], and every other property is optional and may be absent. That makes B a subtype of A, so the union reduces to just A. data.feed was therefore plain FeedResponse, and data.feed.week_start resolved cleanly. The union was always heterogeneous; reduction hid it.
Adding description and image to branch A only breaks the subtype relationship in both directions: B now lacks two properties A has, and A's feed: FeedResponse is not assignable to B's feed: { items: never[] }. No reduction occurs, the union survives into PageData, and every access on data.feed must now be valid for { items: never[] } too. Hence an error on week_start — a property you never touched, reported in a file you never edited.
Fix — type the fallback, not the branches:
if (res.status !== 200) {
const empty_feed: FeedResponse = { items: [] }; // annotation widens it
return { feed: empty_feed, user, title };
}Both constituents now carry feed: FeedResponse, so property access resolves whether or not the outer union reduces. This is more durable than the usual advice of keeping every return branch's key set identical: that only works until the next time someone adds a field to one branch, and it forces you to thread placeholder keys through error paths forever.
Gotcha: satisfies does not fix this. { items: [] } satisfies FeedResponse still infers the narrow literal type { items: never[] } — satisfies checks assignability without widening. You need a type annotation on a const, or an as FeedResponse cast.
Detection: if svelte-check or tsc suddenly errors on a property you did not touch, and the reported type is a union containing a hand-written fallback literal, look for never[] or never in that constituent. never[] is the fingerprint of an empty array literal in a fallback, and its presence in an error message means union reduction just stopped happening. Note the corollary: this class of bug is latent in any codebase whose load functions return a bare { items: [] } fallback — it type-checks today only because reduction is masking it.
Related but distinct: SvelteKit universal layout load union types cause TypeScript errors for optional data covers branches that return different key sets from the start, where the union never reduces and the error appears immediately. This case is the opposite — identical key sets, clean type-check, and a delayed failure triggered later by an unrelated-looking edit.