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
// 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
}// 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);
}
override get total(): number {
return super.total * (1 - this.discount.value);
}
override 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;
}// 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);
}
override get total(): number {
return super.total * (1 + this.taxRate.value);
}
override 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
// ComputedProduct.ts — level 1: memoizes the base total.
import { computed, ref } from 'vue';
import { Reactive } from '../../ivue';
class $ComputedProduct {
get price() {
return ref(48);
}
// computed: stable-handle — the chain below reads super.total.value
get total() {
return computed(() => this.computeBaseTotal());
}
computeBaseTotal() {
return this.price.value;
}
}
export namespace ComputedProduct {
export const $Class = $ComputedProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}// 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);
}
// computed: stable-handle — caches its refinement of the parent cell
override get total() {
return computed(() => this.computeDiscountedTotal());
}
get baseTotal() {
return super.total.value;
}
computeDiscountedTotal() {
return super.total.value * (1 - this.discount.value);
}
baseTotalCell() {
return super.total;
}
}
export namespace ComputedSaleProduct {
export const $Class = $ComputedSaleProduct;
export let Class = Reactive($Class);
export type Instance = typeof Class.Instance;
}// 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);
}
// computed: stable-handle — caches tax over the middle-level cell
override get total() {
return computed(() => this.computeTaxedTotal());
}
get discountedTotal() {
return super.total.value;
}
computeTaxedTotal() {
return super.total.value * (1 + this.taxRate.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:
// InheritanceExample.ts — the route's ONE model. It hosts both product
// chains and forwards the refs the template binds, so one instance drives
// both panes and every label and action is named.
import { Reactive } from '../../ivue';
import type { Ref } from 'vue';
import { ComputedTaxedProduct } from './ComputedTaxedProduct';
import { TaxedProduct } from './TaxedProduct';
class $InheritanceExample {
// HOSTED models — created on first touch, held for the life of the route
protected get $product() {
return new TaxedProduct.Class();
}
protected get $computedProduct() {
return new ComputedTaxedProduct.Class();
}
/** The chains, exposed for the template's dotted reads. */
get product() {
return this.$product;
}
get computedProduct() {
return this.$computedProduct;
}
// FORWARDED cells — the plain-getter chain's refs
get price(): Ref<number> {
return this.$product.price;
}
get discount(): Ref<number> {
return this.$product.discount;
}
get taxRate(): Ref<number> {
return this.$product.taxRate;
}
// FORWARDED cells — the computed chain's refs
get computedPrice(): Ref<number> {
return this.$computedProduct.price;
}
get computedDiscount(): Ref<number> {
return this.$computedProduct.discount;
}
get computedTaxRate(): Ref<number> {
return this.$computedProduct.taxRate;
}
// DERIVED — the labels, named
get receiptLines() {
return this.$product.receipt();
}
get discountPercent() {
return Math.round(this.discount.value * 100);
}
get taxRatePercent() {
return Math.round(this.taxRate.value * 100);
}
get totalLabel() {
return this.$product.total.toFixed(2);
}
get computedBaseTotalLabel() {
return this.$computedProduct.baseTotal.toFixed(2);
}
get computedDiscountedTotalLabel() {
return this.$computedProduct.discountedTotal.toFixed(2);
}
get computedTotalLabel() {
return this.$computedProduct.total.value.toFixed(2);
}
// ACTIONS — one per button, on the chain they touch
bumpPrice() {
this.price.value += 6;
}
bumpDiscount() {
this.discount.value = Math.min(this.discount.value + 0.05, 0.9);
}
toggleTax() {
this.taxRate.value = this.taxRate.value ? 0 : 0.1;
}
bumpComputedPrice() {
this.computedPrice.value += 6;
}
bumpComputedDiscount() {
this.computedDiscount.value = Math.min(this.computedDiscount.value + 0.05, 0.9);
}
toggleComputedTax() {
this.computedTaxRate.value = this.computedTaxRate.value ? 0 : 0.1;
}
}
export namespace InheritanceExample {
export const $Class = $InheritanceExample; // raw — children `extends` this
export let Class = Reactive($Class); // reactive — you `new` this
export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
}<script setup lang="ts">
import DemoBox from './DemoBox.vue';
import { InheritanceExample } from '@examples/inheritance/InheritanceExample';
const example = new InheritanceExample.Class();
// the state destructure
const {
// forwarded cells — the plain-getter chain
price
} = example;
</script>
<template>
<DemoBox
title="Three files, three levels, one instance"
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."
>
<div class="receipt d-mono">
<div v-for="(line, index) in example.receiptLines" :key="index">
{{ line }}
</div>
</div>
<div class="d-vals d-vals-2col">
<div>
<div class="d-k">price · Product</div>
<div class="d-n">${{ price }}</div>
</div>
<div>
<div class="d-k">discount · SaleProduct</div>
<div class="d-n">{{ example.discountPercent }}%</div>
</div>
<div>
<div class="d-k">tax · TaxedProduct</div>
<div class="d-n">{{ example.taxRatePercent }}%</div>
</div>
<div>
<div class="d-k">total · plain getter</div>
<div class="d-n grad">${{ example.totalLabel }}</div>
</div>
</div>
<div class="d-row">
<button class="d-btn primary" type="button" @click="example.bumpPrice()">price +$6</button>
<button class="d-btn" type="button" @click="example.bumpDiscount()">deeper sale</button>
<button class="d-btn" type="button" @click="example.toggleTax()">toggle tax</button>
</div>
</DemoBox>
</template>
<style scoped>
.dbx .d-vals.d-vals-2col {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.d-vals .d-k {
white-space: nowrap;
}
.receipt {
margin-bottom: 16px;
padding: 12px 16px;
border-radius: 10px;
background: color-mix(in srgb, currentColor 4%, transparent);
border: 1px solid rgba(148, 163, 184, 0.16);
font-size: 13px !important;
color: #7dd3fc !important;
line-height: 1.7;
}
.receipt div:last-child {
color: #34d399;
font-weight: 700;
}
</style><script setup lang="ts">
import { InheritanceExample } from '@examples/inheritance/InheritanceExample';
import DemoBox from './DemoBox.vue';
const example = new InheritanceExample.Class();
</script>
<template>
<DemoBox
title="Three levels, three cached cells, one instance"
note="Every class declares a computed named total. The child reads super.total.value, so the base, discounted and taxed cells coexist without colliding."
>
<div class="d-vals d-vals-3col">
<div>
<div class="d-k">Product.total</div>
<div class="d-n">${{ example.computedBaseTotalLabel }}</div>
</div>
<div>
<div class="d-k">SaleProduct.total</div>
<div class="d-n">${{ example.computedDiscountedTotalLabel }}</div>
</div>
<div>
<div class="d-k">TaxedProduct.total</div>
<div class="d-n grad">${{ example.computedTotalLabel }}</div>
</div>
</div>
<div class="d-row">
<button class="d-btn primary" type="button" @click="example.bumpComputedPrice()">
price +$6
</button>
<button class="d-btn" type="button" @click="example.bumpComputedDiscount()">
deeper sale
</button>
<button class="d-btn" type="button" @click="example.toggleComputedTax()">toggle tax</button>
</div>
</DemoBox>
</template>
<style scoped>
.dbx .d-vals.d-vals-3col {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.d-vals .d-k {
white-space: nowrap;
}
@media (max-width: 640px) {
.dbx .d-vals.d-vals-3col {
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.
Related guide pages
- Inheritance & super —
extends $Class,super,override. - Namespace Pattern —
$Class,Class, and the types derived from them. - Reactive State — ref-getters, plain getters, the
$-prefixed cache.