Course outline

Vue / Advanced: architecture, hydration, and production evidence / Lesson 13

Typed reactive contracts and dependency boundaries

Use TypeScript and explicit reactive interfaces to document ownership without mistaking static types for runtime validation.

About 4 min readContent reviewed
On this page

Lesson goals

  • Define typed props and events within the supported SFC compiler.
  • Distinguish readonly, ref unwrapping, and injected context.

Separate static promises from incoming data

TypeScript can describe component inputs, event payloads, refs, and service results before runtime. It does not validate an arbitrary JSON response merely because the result is asserted as an interface. Validate external data at a trusted boundary and convert it into the domain contract. A readonly type can discourage writes during development but does not freeze every object at runtime. Vue’s readonly wrappers have their own runtime behavior and are distinct from TypeScript modifiers. State which protection or documentation each layer provides instead of calling a typed component automatically safe.

Make the component’s public shape explicit

The SFC below uses type-based props and tuple-style event declarations supported by the Vue 3.5 baseline. It receives a note and proposes selection through its id. The parent remains responsible for the actual selection policy and data source. Readonly describes how this component should use the supplied note; it is not an authorization mechanism. Runtime prop inference has compiler limitations, especially for complex imported or conditional types. Use compatible tooling and vue-tsc or the project’s supported SFC type checker rather than expecting ordinary JavaScript syntax checks to validate these declarations.

vue A typed selection contract
<script setup lang="ts">
type Note = { id: string; title: string }
const props = defineProps<{ note: Readonly<Note> }>()
const emit = defineEmits<{ select: [id: string] }>()
function select() { emit("select", props.note.id) }
</script>

<template>
  <button type="button" @click="select">{{ props.note.title }}</button>
</template>

Requires the Vue 3.5 SFC compiler and compatible TypeScript tooling. The emitted payload is the supplied note id. No TypeScript compiler or Vue runtime test was performed; the fragment remains non-runnable and is not claimed to validate remote data.

Understand collection-specific ref access

Template convenience does not make every ref inside every structure disappear. Reactive arrays and native collections have documented ref-unwrapping limits. The example keeps explicit .value access for refs stored in an array and Map. A reviewer should distinguish the collection proxy, the ref element, and the primitive it holds. Passing the primitive out of the collection still creates a snapshot unless the receiver has another live source. Avoid clever nesting that makes ownership obscure. A simpler data model can be easier to validate and maintain than relying on a chain of implicit conversions.

javascript Explicit ref access inside reactive collections
import { reactive, ref } from "vue"

const entries = reactive([ref("n1")])
const lookup = reactive(new Map([["selected", ref("n2")]]))
const firstId = entries[0].value
const selectedId = lookup.get("selected").value
if (firstId !== "n1" || selectedId !== "n2") {
  throw new Error("COLLECTION_REF_MISMATCH")
}

Requires Vue and demonstrates its documented array/Map behavior. The intended assertion succeeds under the stated API contract, but this framework-dependent snippet was not executed. It does not mount or render a component.

Expose mutation authority deliberately

Returning a readonly view can help a composable expose observable state while reserving mutation for named operations. It does not make the underlying owner incapable of updating the source, and it does not secure server data. Decide whether a consumer receives a ref, readonly ref, shallow structure, or plain immutable value and document that choice. Shallow APIs intentionally leave nested values outside deep conversion, which can surprise consumers who mutate them. Review proxy identity when integrating external libraries, and keep raw-object escape hatches limited to a clear reason. Static types should describe these contracts rather than conceal them through broad casts.

Use injected context as an explicit dependency

Provide/inject can pass a capability through a component tree without every intermediate component forwarding props. Use a stable key, preferably a typed symbol where appropriate, and decide how missing providers are handled. Keep mutations near the provider or expose deliberate operations rather than allowing arbitrary consumers to alter shared state. Injection is not automatically global and follows the application/component ownership tree; separate SSR requests still need separate contexts. Tests should supply the intended provider and check missing-context behavior. A hidden ambient dependency is harder to reason about than a documented injected service with a narrow interface.

Common errors and fixes

A type assertion is treated as response validation

Why it happens
Static typing is confused with runtime evidence.
How to fix it
Validate untrusted data before constructing the typed domain value.
Prevent it next time
Keep parsing and type boundaries explicit.

A ref in a Map is read as a primitive

Why it happens
Template unwrapping rules are generalized to collections.
How to fix it
Use the documented explicit ref access.
Prevent it next time
Test the actual collection shape and API contract.

A missing provider causes a distant crash

Why it happens
Injected context requirements are undocumented.
How to fix it
Handle missing context at the dependency boundary.
Prevent it next time
Test provider presence and ownership.

Check your understanding

1. Does Readonly<Note> prove a server response is authorized?

Show worked solution

No. It describes a static usage contract. Server authorization and runtime data validation remain separate responsibilities. The component can receive a typed value only after the service boundary has established the required facts.

2. A composable returns shallow state and consumers mutate nested fields. Why can updates be surprising?

Show worked solution

Shallow conversion does not track every nested mutation like deep reactive state. Document immutable replacement or another explicit update contract, and verify the UI reads the source that is actually triggered.

Key takeaways

  • Types, readonly views, and validation provide different guarantees.
  • Collection ref rules must be read in context.
  • Injected capabilities need ownership and missing-context behavior.

Official references