Back to Blog

November 3, 2026

OWL Now Validates What’s Inside Your Sets and Maps, Not Just That They Exist

Odoo’s OWL framework introduces native t.set() and t.map() type validators that check every element, key, and value at runtime and infer proper generic types, while a companion fix to asyncComputed.refresh() closes a timing gap that let stale data slip through awaited refreshes.

Diagram showing OWL's new t.set() and t.map() type validators with proper generic inference, alongside a before-and-after timeline of the asyncComputed.refresh() timing fix

If you’ve built anything non-trivial with OWL — Odoo’s reactive component framework — you’ve probably hit the moment where you need to validate a Set or a Map passed as a prop. Until now, the best you could do was t.instanceOf(Set), which confirmed the container type but told you absolutely nothing about what was inside it. Your editor inferred Set<unknown>, autocomplete gave up, and any type error in the contents sailed through validation without a peep. That era is over. OWL now ships native t.set() and t.map()validators that check every element, every key, and every value — and resolve to proper generic types that your tooling can actually use.

The Problem With instanceOf

The previous approach wasn’t wrong, exactly. Calling t.instanceOf(Set)did verify that the value was a Set. But in a typed codebase, “it’s a Set” is only half the story. A Set<number> and a Set<string> are fundamentally different data structures in terms of what you can do with them, and instanceOf treated them identically. The inferred type collapsed to Set<unknown>, which meant every downstream operation required a cast or a guard. You could reach for t.customValidator to write your own content check, but the validator still returned the base type, so your editor remained just as confused.

For Maps, the situation was worse. A Map has two type parameters — key and value — and instanceOf couldn’t express either one. Developers who needed type-safe Maps in OWL components were stuck writing manual runtime checks that the type system couldn’t see, creating a gap between what the code enforced and what the editor understood.

t.set() and t.map(): Content-Aware Validation

The new validators work exactly the way you’d expect them to. Calling t.set(t.number()) validates that the value is a Set, then iterates every element and checks it against the provided type. The inferred type resolves to Set<number>— real generic inference, not a cast. If you omit the value type and just call t.set(), you get a Set<any> — still better than unknown, and honest about what it knows.

Maps follow the same pattern with two parameters: t.map(t.string(), t.number()) validates both keys and values across every entry and resolves to Map<string, number>. You provide both type arguments or neither — there’s no halfway. This mirrors how TypeScript’s own Map type works: you declare both dimensions or you accept the defaults.

Error Reporting That Makes Sense for Collections

Validating collections introduces a problem that flat object validation doesn’t have: how do you point to the failing entry? With an object, you name the property. With an array, you name the index. But a Map key can be anything — an object, a number, another Map — so there’s no universal “name” for an entry.

OWL solves this by using the iteration index. If the third entry in your Map has an invalid key, the error path reads 2 > key. If its value is the problem, you get 2 > value. This is pragmatic rather than pretty — iteration order in a Map is insertion order, so the index is stable and reproducible. For Sets, where there are no keys, the error simply names the index of the failing element.

Under the hood, the validation context gains a new withEntry(key, value)method. This exists because Set elements and Map keys can’t be looked up by key on the validated value the way object properties can. The validation framework needed a new primitive to walk these structures, and now it has one.

The asyncComputed Timing Fix Nobody Noticed They Needed

Alongside the type system improvements, OWL quietly fixed a subtle timing bug in asyncComputed that could cause awaited refreshes to resolve with stale data. The issue was deceptively simple: calling refresh()wrote a signal but didn’t start the fetch. The actual fetcher wouldn’t run until the next effect flush — a microtask later. If you wrote await data.refresh(), the await could resolve before the new fetch even started, handing you the old value while the fresh one was still in flight.

The fix changes refresh() to return a proper promise that resolves when the refreshed value is ready. It starts the run synchronously, immediately marking it as in-flight and setting loading() to true. It bumps an internal run ID so that any superseded fetch is correctly discarded. And after dispose(), it returns a resolved promise instead of hanging on a fetch that will never complete.

Why the Loading State Matters

The behavioral change that will affect the most components is subtle but important: loading() now becomes true the instant refresh() returns, not one microtask later. Before the fix, there was a single-tick window where a component had called refresh but loading() still reported false. Any UI that conditionally showed a spinner based on the loading state could flash the stale data for one frame before the spinner appeared.

For most applications, this was invisible. But for components doing rapid sequential refreshes — a search-as-you-type field backed by an async data source, for instance — the old behavior could produce visible flicker as the loading indicator lagged behind the actual state. The fix eliminates that lag entirely. When you call refresh, the component knows it’s loading, right now, with no gap.

What This Means for OWL Developers

These two changes address different layers of the framework but share a common theme: closing gaps between what the code does and what the developer sees. The type validators make the runtime behavior visible to the type system. The async fix makes the loading state visible to the component at the exact right moment. Neither change breaks existing code — t.instanceOf(Set) still works, and refresh()still triggers a re-fetch — but both provide strictly more accurate information to the code that consumes them.

For teams maintaining large OWL codebases, the migration path is straightforward. Replace t.instanceOf(Set) calls with t.set() and provide the element type. Replace t.instanceOf(Map) with t.map() and specify both key and value types. Your editor will immediately start offering correct completions on the validated props, and any content that doesn’t match the declared types will surface at validation time instead of producing mysterious failures three components downstream.

Ready to experience Odoo AI?

Join hundreds of teams using DearERP to customize Odoo in minutes, not weeks. Plans start at $29/month.