Common Types

UnitPrice

The price-per-unit type that pairs a Money value with a quantity and a reference measurement, e.g. "€13.99 / 100g".

A 330ml bottle priced at €4.20 also shows "€12.73 / 1l" on the shelf. A 500g bag of coffee shows "€19.98 / kg". These per-unit labels are how customers compare prices across pack sizes, and in many EU markets they are required by law on grocery and household goods. The shape connectors return for them is UnitPrice: a Money value, the quantity the variant actually contains, and the reference quantity the price is normalized to.

import type { UnitPrice } from '@laioutr-core/core-types/common';

interface UnitPrice {
  price: Money;            // cost for the reference quantity
  quantity: Measurement;   // the variant's actual content (e.g. 330ml)
  reference: Measurement;  // the unit shown to the customer (e.g. 100ml or 1l)
}

UnitPrice is a flat record, not a discriminated union. Variants would not pay their way: the rendering convention ("price slash unit") is the same whether the product is a drink, a detergent, or a bag of flour.

Fields

price

A Money value carrying the cost for the reference quantity, not the variant's actual content. A 330ml bottle that retails at €4.20 with a per-100ml unit price has price: { amount: 127, currency: 'EUR' } (€1.27 per 100ml), regardless of how many ml the variant contains.

The currency must match the rest of the entity's pricing. Mixing a USD unitPrice.price with a EUR prices.price is structurally possible but invalid; the storefront has one currency per market.

quantity

A Measurement ({ value: number, unit: string }) describing how much of the product the variant actually contains. For a 330ml bottle of soda, quantity: { value: 330, unit: 'ml' }. For a 500g bag of coffee, { value: 500, unit: 'g' }. This is informational; the renderer does not divide by it.

reference

The unit the price is normalized to. This is the value shown after the slash on the shelf label, the one customers compare across products. Conventions vary by category:

Product categoryCommon referenceExample label
Beverages100ml or 1l€1.27 / 100ml
Solid food100g or 1kg€19.98 / kg
Detergents1l or 1kg€2.45 / l
Paper goods1sheet or 100sheet€0.04 / sheet
Wine, spirits750ml (the bottle), 1l€18.66 / 750ml

Stick to the reference that customers expect for the category. The unit field accepts the ECMA-402 sanctioned identifiers plus the Laioutr-known units ('ml', 'l', 'g', 'kg', 'sheet', 'item', etc.). Custom strings work but may not localize correctly.

Rendering UnitPrice

A UnitPrice value is just data. The renderer pairs the global $money and $measurement formatters:

components/PriceWithUnit.vue
<script setup lang="ts">
import type { UnitPrice } from '@laioutr-core/core-types/common';
const props = defineProps<{ unitPrice: UnitPrice }>();
</script>

<template>
  <span>{{ $money(unitPrice.price) }} / {{ $measurement(unitPrice.reference) }}</span>
  <!-- Renders 'CHF 1.27 / 100ml', '€19.98 / kg', etc. -->
</template>

Both formatters read the active locale, so the label localizes alongside the rest of the page. The UI Kit's product tiles (ProductTileBasic, ProductTileBasicReverse) and cart line items (CartListItem) emit this label automatically when their unitPrice prop is set.

For connector authors

When the source backend has a unit price (Shopify exposes it on ProductVariant.unitPrice and ProductVariant.unitPriceMeasurement; Adobe Commerce surfaces it on attribute-driven products; Hygraph and Storyblok let editors enter it manually), map the three fields directly:

import type { UnitPrice } from '@laioutr-core/core-types/common';
import { Money } from '@screeny05/ts-money';

const toUnitPrice = (raw: ShopifyUnitPrice): UnitPrice => ({
  price: Money.fromDecimal(raw.amount, raw.currencyCode),
  quantity: { value: raw.measuredVariant.value, unit: raw.measuredVariant.unit },
  reference: { value: raw.measuredReference.value, unit: raw.measuredReference.unit },
});

If the backend gives you only the variant's pack size and a per-100g convention, compute the price yourself rather than dropping the field entirely:

const variantWeightG = 500;     // 500g pack
const variantPriceCents = 1499; // €14.99
const referenceG = 100;         // per 100g

const unitPrice: UnitPrice = {
  price: { amount: Math.round(variantPriceCents * referenceG / variantWeightG), currency: 'EUR' },
  quantity: { value: variantWeightG, unit: 'g' },
  reference: { value: referenceG, unit: 'g' },
};
// price.amount === 300, renders as '€3.00 / 100g'

When the backend has no unit-price data at all, leave the field undefined. The storefront skips the label cleanly; do not fabricate one from the headline price alone, since that produces a label customers cannot compare against competitor products in the same category.

Copyright © 2026 Laioutr GmbH