The Release With No New Features (And Why That's a Good Thing) - Elyra v0.9.11
Elyra v0.9.11 ships zero new features and deletes 143 any types from @elyracode/web-ui. Here's why a types-only release is some of the most valuable work we do — with real before-and-after from the diff.
Every changelog has that one release where the "Added" section is empty and the "Changed" section says something like "internal type safety." Most people scroll right past it. This is a post in defense of that release — because v0.9.11 is exactly that release, and the work behind it is some of the most valuable we've shipped.
No new commands. No new models you didn't already have. What changed is that we deleted 143 any types from @elyracode/web-ui and replaced every one with a type that actually describes the data. Zero behavior change. Here's why that matters, and how we did it, with real before-and-after from the diff.
Why bother? any is a silent "trust me"
In TypeScript, any is an escape hatch that says "stop checking, I know what I'm doing." The problem is that it doesn't just turn off checking for that one value — it spreads. Every property you read off an any is also any. Every function it flows into loses its guarantees. One any in the wrong place quietly disables the type checker across a whole call path.
For an embeddable UI package like web-ui — the one you drop into your own app to get an Elyra chat panel — that's a real cost. Consumers rely on our types to know what they're wiring up. An any is a place where the editor stops helping them and a refactor can break things without anyone noticing until runtime.
So the "why" is simple: 143 places where the compiler had been told to look away, now look back. Let me show you what that actually looked like.
How: four patterns that covered almost everything
1. Give the message protocol a real shape
The biggest cluster lived in the sandbox runtime — the bridge that lets sandboxed code talk to the host page. Messages were flying around as any, so every handler was guessing.
We introduced a shared runtime-messages module with a discriminated union. Now a message is one of a known set of shapes:
export type ArtifactOperationMessage =
| { type: "artifact-operation"; action: "list" }
| { type: "artifact-operation"; action: "get"; filename: string }
| { type: "artifact-operation"; action: "delete"; filename: string }
| { type: "artifact-operation"; action: "createOrUpdate";
filename: string; content: string; mimeType?: string };
The payoff shows up in the handler. Before, message.filename was a hopeful guess. Now, after you check message.action === "get", TypeScript knows filename exists — and would error if you tried to read it in the "list" branch where it doesn't. The compiler enforces the protocol you designed, instead of you remembering it.
2. Narrow instead of asserting
A common shortcut is to cast away a union you don't want to deal with. Here's the PDF text extraction, before:
const pageText = textContent.items
.map((item: any) => item.str) // some items have .str, some don't
.filter((str: string) => str.trim())
PDF.js returns a mix of text items and "marked content" items; only the former have .str. The any papered over that. The fix narrows instead:
const pageText = textContent.items
.map((item) => ("str" in item ? item.str : ""))
.filter((str) => str.trim())
Same runtime behavior — but now the code is honest about the fact that not every item has a string, and the compiler verifies we handle both.
3. Type the dynamic data once, at the edge
Sometimes the data genuinely is loosely shaped — like the document tree from docx-preview, whose own types literally declare WordDocument = any. You can't avoid the dynamism, but you can contain it. We described the shape we actually walk:
interface DocxElement {
type?: string;
text?: string;
children?: DocxElement[];
}
function extractTextFromElement(element: DocxElement): string { ... }
One interface at the boundary, and the entire recursive walk below it is type-checked. The any stops at the door instead of leaking into every line.
4. Stop lying in catch
These were everywhere:
} catch (error: any) {
this.error = error?.message || i18n("Failed to load PDF");
}
error: any lets you reach for .message on something that might be a string, a number, or anything a thrown value can be. The honest version:
} catch (error) {
this.error = (error instanceof Error ? error.message : undefined)
|| i18n("Failed to load PDF");
}
Now error is unknown, and we only read .message after proving it's an Error. Slightly more typing; a lot more truthful.
Bonus: the cast that wasn't needed
My favorite kind of cleanup is the one where the any was hiding a better API the whole time. This tested a provider key:
let model = getModel(provider as any, modelId); // fighting the generics
// ...
const result = await complete(model, context, { apiKey, maxTokens } as any);
Two anys, both bypassing the type system to force a square peg through. It turned out there was a round hole:
let model = getModels(provider as KnownProvider).find((m) => m.id === modelId);
// ...
const result = await complete(model, context, { apiKey, maxTokens });
getModels() returns a typed array, .find() returns Model | undefined, and complete's options already accept apiKey and maxTokens — so the second as any was pure superstition. Removing any didn't just satisfy the linter; it led us to simpler, more correct code. That happens more often than you'd think: an any is frequently a sign you're using an API the hard way.
How we kept it honest
The rule for this kind of work is the same as the rule for any itself: don't cheat. No swapping one escape hatch for another. So:
Every change was verified with
npm run check(Biome +tsc) — green on every commit.No behavior changes. This was a types-only pass; the emitted JavaScript is effectively identical.
We didn't reach for
// biome-ignoreto make red squiggles disappear. The count of remaininganyinweb-uiis now zero, honestly earned.
We also folded in a routine model-registry refresh while we were here (a few price updates plus some new image and GLM models), so the data stays current.
The takeaway
A release with an empty "Added" section isn't a slow week — sometimes it's the week you pay down the quiet debt that makes every future feature safer to build. 143 fewer places where the compiler was told to look away means 143 fewer places a refactor can silently break, and a web-ui package whose types now tell consumers the truth.
If you build on @elyracode/web-ui, you don't have to do anything. Just update, and enjoy an editor that helps you a little more than it did last week. That's the whole feature: nothing changed, and everything got a little more trustworthy.