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

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

  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.

Released under the MIT License.