Skip to content

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 total chain 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);
  }

  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;
}
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);
  }

  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 total cells 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);
  }

  // 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;
}
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);
  }

  // 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;
}
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);
  }

  // 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:

ts
// 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
}
vue
<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 &middot; Product</div>
        <div class="d-n">${{ price }}</div>
      </div>
      <div>
        <div class="d-k">discount &middot; SaleProduct</div>
        <div class="d-n">{{ example.discountPercent }}%</div>
      </div>
      <div>
        <div class="d-k">tax &middot; TaxedProduct</div>
        <div class="d-n">{{ example.taxRatePercent }}%</div>
      </div>
      <div>
        <div class="d-k">total &middot; 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>
vue
<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.

Released under the MIT License.