Inheritance chain
Three classes, each in its own file, each refining total through super.total — and every receipt() line written by a different level of the chain. Children extend the RAW class (Product.$Class), and each file exports its own Reactive() wrapper through the namespace.
What to notice
- Zero computeds. This
totalchain is entirely plain getters — reactive through leaf tracking, with no per-instance allocation at any level. - Write anywhere, everything re-derives. Push
price, deepen the discount or toggle the tax — the receipt and total update from whichever level you touched.
Plain-getter source
ts
// Product.ts — level 1: knows its title and price.
import { Reactive } from '../../ivue';
import { ref } from 'vue';
class $Product {
get title() {
return ref('Mechanical keyboard');
}
get price() {
return ref(48);
}
get total(): number {
return this.price.value;
}
receipt(): string[] {
return [`${this.title.value} — $${this.price.value.toFixed(2)}`];
}
}
export namespace Product {
export const $Class = $Product; // raw — children `extends` this
export let Class = Reactive($Class); // reactive — you `new` this
export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
}ts
// SaleProduct.ts — level 2: applies a discount to whatever the parent says.
import { Reactive } from '../../ivue';
import { ref } from 'vue';
import { Product } from './Product';
class $SaleProduct extends Product.$Class {
get discount() {
return ref(0.2);
}
get total(): number {
return super.total * (1 - this.discount.value);
}
receipt(): string[] {
return [
...super.receipt(),
`sale −${Math.round(this.discount.value * 100)}%`,
];
}
}
export namespace SaleProduct {
export const $Class = $SaleProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}ts
// TaxedProduct.ts — level 3: adds tax on top of the discounted total.
import { Reactive } from '../../ivue';
import { ref } from 'vue';
import { SaleProduct } from './SaleProduct';
class $TaxedProduct extends SaleProduct.$Class {
get taxRate() {
return ref(0.1);
}
get total(): number {
return super.total * (1 + this.taxRate.value);
}
receipt(): string[] {
return [
...super.receipt(),
`tax +${Math.round(this.taxRate.value * 100)}%`,
`due — $${this.total.toFixed(2)}`,
];
}
}
export namespace TaxedProduct {
export const $Class = $TaxedProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}The computed chain
The same hierarchy can memoize at every level. Each class owns a different computed named total, and each child refines the parent cell through super.total.value:
What to notice
- Same-name computeds coexist. The computed version retains three cached
totalcells on one instance; each prototype level owns its own cache key.
Computed source
ts
// ComputedProduct.ts — level 1: memoizes the base total.
import { computed, ref } from 'vue';
import { Reactive } from '../../ivue';
class $ComputedProduct {
get price() {
return ref(48);
}
get total() {
return computed(() => this.price.value);
}
}
export namespace ComputedProduct {
export const $Class = $ComputedProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}ts
// ComputedSaleProduct.ts — level 2: caches its refinement of the parent cell.
import { computed, ref } from 'vue';
import { Reactive } from '../../ivue';
import { ComputedProduct } from './ComputedProduct';
class $ComputedSaleProduct extends ComputedProduct.$Class {
get discount() {
return ref(0.2);
}
get total() {
return computed(() => super.total.value * (1 - this.discount.value));
}
get baseTotal() {
return super.total.value;
}
baseTotalCell() {
return super.total;
}
}
export namespace ComputedSaleProduct {
export const $Class = $ComputedSaleProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}ts
// ComputedTaxedProduct.ts — level 3: caches tax over the middle-level cell.
import { computed, ref } from 'vue';
import { Reactive } from '../../ivue';
import { ComputedSaleProduct } from './ComputedSaleProduct';
class $ComputedTaxedProduct extends ComputedSaleProduct.$Class {
get taxRate() {
return ref(0.1);
}
get total() {
return computed(() => super.total.value * (1 + this.taxRate.value));
}
get discountedTotal() {
return super.total.value;
}
discountedTotalCell() {
return super.total;
}
}
export namespace ComputedTaxedProduct {
export const $Class = $ComputedTaxedProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}Playground wrapper
The standalone playground renders both hierarchies on one route:
vue
<script setup lang="ts">
import { ComputedTaxedProduct } from './ComputedTaxedProduct';
import { TaxedProduct } from './TaxedProduct';
const product = new TaxedProduct.Class();
const computedProduct = new ComputedTaxedProduct.Class();
// the state destructure
const {
// state refs
price,
discount,
taxRate,
} = product;
const {
price: computedPrice,
discount: computedDiscount,
taxRate: computedTaxRate,
total: computedTotal,
} = computedProduct;
</script>
<template>
<div class="inheritance-stack">
<div class="pane">
<p class="note">
total is a plain-getter chain — each level refines super.total, zero
computeds allocated. Every receipt() line is written by a different
class in the chain. Write to any level's ref and everything re-derives.
</p>
<div class="receipt">
<div v-for="(line, index) in product.receipt()" :key="index">
{{ line }}
</div>
</div>
<div class="vals">
<div>
<div class="k">price · Product</div>
<div class="n">${{ price }}</div>
</div>
<div>
<div class="k">discount · SaleProduct</div>
<div class="n">{{ Math.round(discount * 100) }}%</div>
</div>
<div>
<div class="k">tax · TaxedProduct</div>
<div class="n">{{ Math.round(taxRate * 100) }}%</div>
</div>
<div>
<div class="k">total · plain getter</div>
<div class="n grad">${{ product.total.toFixed(2) }}</div>
</div>
</div>
<div class="row">
<button class="btn primary" type="button" @click="price += 6">
price +$6
</button>
<button
class="btn"
type="button"
@click="discount = Math.min(discount + 0.05, 0.9)"
>
deeper sale
</button>
<button class="btn" type="button" @click="taxRate = taxRate ? 0 : 0.1">
toggle tax
</button>
</div>
</div>
<div class="pane">
<p class="note">
Every level now declares a computed named total. The child reads
super.total.value, so all three cached cells coexist on the same
instance instead of overwriting one another.
</p>
<div class="vals computed-vals">
<div>
<div class="k">Product.total</div>
<div class="n">${{ computedProduct.baseTotal.toFixed(2) }}</div>
</div>
<div>
<div class="k">SaleProduct.total</div>
<div class="n">${{ computedProduct.discountedTotal.toFixed(2) }}</div>
</div>
<div>
<div class="k">TaxedProduct.total</div>
<div class="n grad">${{ computedTotal.toFixed(2) }}</div>
</div>
</div>
<div class="row">
<button class="btn primary" type="button" @click="computedPrice += 6">
price +$6
</button>
<button
class="btn"
type="button"
@click="computedDiscount = Math.min(computedDiscount + 0.05, 0.9)"
>
deeper sale
</button>
<button
class="btn"
type="button"
@click="computedTaxRate = computedTaxRate ? 0 : 0.1"
>
toggle tax
</button>
</div>
</div>
</div>
</template>
<style scoped src="../example-pane.css"></style>
<style scoped>
.inheritance-stack {
display: grid;
gap: 22px;
}
.computed-vals {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@media (max-width: 620px) {
.computed-vals {
grid-template-columns: 1fr;
}
}
</style>Open in StackBlitz ⚡ — the playground boots with this example's route and file active.
For the full story of why inheritance works this way, read the Inheritance guide.