Fetching latest headlines…

Dev

Modelling Swedish intestate succession in TypeScript

Dev.toUnited States · NORTH AMERICA

When someone in Sweden dies without a will, the Inheritance Code (ärvdabalken, SFS 1958:637) decides who gets what. The rules fit on two printed pages. They are also a small, well-defined algorithm: a...

2 views0 likes0 comments

When someone in Sweden dies without a will, the Inheritance Code (ärvdabalken, SFS 1958:637) decides who gets what. The rules fit on two printed pages. They are also a small, well-defined algorithm: a walk over a family tree with a fixed priority order, equal splits per branch, and a couple of special cases for spouses.

I wrote that algorithm in TypeScript for a free calculator that answers "who inherits?" for Swedish users. This post explains the law for developers who do not read Swedish, and shows how each rule maps to code. The identifiers are Swedish because the output is Swedish, so I translate them as they come up.

The engine is one pure function, vemArver(input) ("who inherits"). No DOM, no I/O. It runs in the browser and under node:test.

The law in one screen

Chapter 2 of ärvdabalken defines three classes of relatives (arvsklasser). A class only inherits if every class above it is empty.

  1. First class (2 kap. 1 §). The deceased's descendants. Children take equal shares. If a child is dead, that child's descendants step into its place, and each branch takes an equal share. This is called istadarätt, the right of representation. English legal texts call it per stirpes.
  2. Second class (2 kap. 2 §). Parents take half each. If a parent is dead, the deceased's siblings share that parent's half, and a dead sibling's descendants step into its place. If there are no siblings or descendants of siblings, a living parent takes everything.
  3. Third class (2 kap. 3 §). Grandparents take a quarter each. A dead grandparent's share goes to that grandparent's children (the deceased's aunts and uncles). If there are none, the other grandparent on the same side takes it. If one side has nobody, the other side takes everything.

Then two short rules close it off. 2 kap. 4 § says no other relatives inherit. Cousins get nothing. And 5 kap. 1 § says that if there is no heir under these rules, the estate goes to Allmänna arvsfonden, the state inheritance fund.

Chapter 3 then adds the surviving spouse, who inherits ahead of the children in most cases. More on that below, because it is where most of the interesting logic is.

Exact fractions, not floats

Before any tree walking, one decision shapes everything else. Shares are fractions, and the answer the user needs is "1/6", not "0.16666666666666666".

Floats fail here in two ways. First, display: a grandchild who takes one sixth must see one sixth. Second, invariants: the shares must sum to exactly 1, and with floats 1/3 + 1/3 + 1/6 + 1/6 is not reliably equal to 1. You end up with epsilon comparisons in your tests, and an epsilon test cannot tell a rounding artefact from a real bug where a branch got the wrong lot.

So the engine carries a tiny rational type and normalises on every construction:

export interface Frac { n: number; d: number }

const gcd = (a: number, b: number): number => (b === 0 ? Math.abs(a) : gcd(b, a % b));
export const frac = (n: number, d = 1): Frac => {
  if (d === 0) throw new Error('division by zero');
  if (n === 0) return { n: 0, d: 1 };
  const g = gcd(n, d);
  const s = d < 0 ? -1 : 1;
  return { n: (s * n) / g, d: (s * d) / g };
};
export const add = (a: Frac, b: Frac): Frac => frac(a.n * b.d + b.n * a.d, a.d * b.d);
export const sub = (a: Frac, b: Frac): Frac => frac(a.n * b.d - b.n * a.d, a.d * b.d);
export const mul = (a: Frac, b: Frac): Frac => frac(a.n * b.n, a.d * b.d);
export const div = (a: Frac, k: number): Frac => frac(a.n, a.d * k);
export const eq = (a: Frac, b: Frac) => a.n === b.n && a.d === b.d;

Because every value is reduced and the sign lives in the numerator, equality is a plain field comparison. frac(2, 6) and frac(1, 3) are the same object shape, so eq and deepEqual both work without a custom comparator.

Plain number is enough here. Input counts are clamped to 50 per branch, and the deepest split is roughly a quarter divided by a sibling count, so numerators and denominators stay tiny. If your domain can nest arbitrarily, use bigint for the same code.

Percentages are only computed at the very edge, for display:

export const fmtPct = (f: Frac) => {
  const v = Math.round((f.n / f.d) * 1000) / 10;
  return `${String(v).replace('.', ',')} %`;
};

The output shape: a line per branch

Each heir in the result is a line, not a person. A dead child with three children produces one line with antal: 3 (count), a share per person (var) and a share for the whole branch (totalt):

export interface Arvinge {
  key: string;
  label: string;
  antal: number;   // how many people share this line equally
  var: Frac;       // share of the estate for each person
  totalt: Frac;    // share for the whole line (var * antal)
  lagrum: string;  // the statute section that produced it
}

Two details matter. lagrum (the legal citation) travels with every line, so the UI can show "2 kap. 1 § 2 st ärvdabalken (istadarätt)" next to the number. A user, or a lawyer they show it to, can check where each share came from. And key is stable (barn-3, syskon-2), which is what lets the same people be merged when two rules hand them shares.

First class: children and representation

The first class is a single pass. A child who is alive is a branch. A dead child with living children is a branch. A dead child with no children is not a branch at all, and its share silently goes to the others, because the law divides among branches that exist.

function forstaKlassen(barn: Barn[]) {
  const grenar = barn
    .map((b, index) => ({ index, barn: { ...b, barn: clampCount(b.barn) } }))
    .filter((g) => g.barn.lever || g.barn.barn > 0);
  if (grenar.length === 0) return { lines: [], grenar };
  const lot = frac(1, grenar.length);
  const lines = grenar.map(({ index, barn: b }) => {
    const n = index + 1;
    if (b.lever) {
      return { key: `barn-${n}`, label: `Barn ${n}`, antal: 1, var: lot, totalt: lot,
               lagrum: '2 kap. 1 § ärvdabalken' };
    }
    return {
      key: `barnbarn-${n}`,
      label: `Barnbarn genom barn ${n} (${b.barn} st)`,
      antal: b.barn,
      var: div(lot, b.barn),
      totalt: lot,
      lagrum: '2 kap. 1 § 2 st ärvdabalken (istadarätt)',
    };
  });
  return { lines, grenar };
}

(grenar means branches, lever means alive, barnbarn means grandchildren.)

The filter is the whole of the representation rule. Count the branches first, then divide. Two living children and one dead child with two kids gives three branches: 1/3, 1/3, and 1/3 split into 1/6 each. If you divide by the number of children instead of the number of live branches, a childless dead child keeps eating a share that belongs to its siblings.

Second class: two halves and half siblings

The second class is easier to reason about if you stop thinking "parents first, then siblings" and start thinking "two halves of the estate, one per parent". Each half is resolved independently:

  • the parent is alive: the parent takes the half
  • the parent is dead: the siblings who share that parent split the half, with representation for dead siblings
  • nobody is left on that side: the half moves to the other side

That framing makes half siblings fall out naturally. 2 kap. 2 § 3 st says half siblings take part only in the share that would have gone to their own parent. A full sibling shares both parents, so a full sibling appears in both halves. A half sibling on the father's side appears only in the father's half.

function syskonPaSida(syskon: Syskon[], sida: 'mor' | 'far') {
  return syskon
    .map((s, index) => ({ index, s: { ...s, barn: clampCount(s.barn) } }))
    .filter(({ s }) =>
      (s.typ === 'hel' || s.typ === (sida === 'mor' ? 'halvMor' : 'halvFar')) &&
      (s.lever || s.barn > 0));
}

(mor is mother, far is father, hel is full sibling, halvMor and halvFar are half siblings through the mother or father.)

Each side returns either a list of lines or null for "nobody here". The combining step is where the fallback rules live:

const m = sideLines('mor');
const f = sideLines('far');
if (!m && !f) return [];
if (!m) return f!.length === 1 && f![0].key === 'far'
  ? [{ ...f![0], var: ONE, totalt: ONE, lagrum: '2 kap. 2 § 2 st ärvdabalken' }]
  : merge(scale(f!, frac(2)));
if (!f) return /* mirror image */;
return merge([...m, ...f]);

If the mother's half has nobody and the father is alive, the father takes everything. If the mother's half has nobody and the father is dead, the father's siblings get their half scaled up to the whole estate. That second case is the last sentence of 2 kap. 2 § 3 st: with no full siblings and both parents dead, the half siblings take the entire estate.

Then merge collapses lines that have the same key. A full sibling gets a quarter from each half when both parents are dead, and the user should see one line with 1/2, not two lines with 1/4:

function merge(list: Arvinge[]): Arvinge[] {
  const out = new Map<string, Arvinge>();
  for (const a of list) {
    if (isZero(a.totalt)) continue;
    const prev = out.get(a.key);
    if (!prev) { out.set(a.key, { ...a }); continue; }
    const totalt = add(prev.totalt, a.totalt);
    out.set(a.key, { ...prev, totalt, var: div(totalt, prev.antal) });
  }
  return [...out.values()];
}

Worked example: both parents dead, one full sibling and one half sibling on the mother's side. The father's half goes only to the full sibling (1/2). The mother's half is split by both (1/4 each). After merging: full sibling 3/4, half sibling 1/4. That is exactly the kind of number a float-based version would print as 0.75 and a user would not be able to check by hand.

Third class: grandparents and their children

The third class repeats the same pattern one generation up, with quarters instead of halves. Each grandparent owns a quarter. A dead grandparent's quarter goes to their children, who are the deceased's aunts and uncles. If there are no such children, the living grandparent on the same side takes the side's half. If a whole side is empty, the other side is scaled by two.

There is no representation below aunts and uncles in this class. Their children are cousins, and 2 kap. 4 § cuts cousins off completely. The code never models cousins at all. Nothing to compute means nothing to get wrong.

The surviving spouse: inheritance now, inheritance later

Here Swedish law departs from most other systems, and here the data model needs a second output.

3 kap. 1 § says that a married person's estate goes to the surviving spouse. The children do not lose their inheritance. They wait. When the surviving spouse later dies, the first deceased's heirs take a share of the spouse's estate. This deferred share is called efterarv, literally "after-inheritance". 3 kap. 2 § sets it as half of the spouse's estate as a main rule, but adjusted to whatever proportion the spouse actually received as inheritance.

The catch is särkullbarn: a child of the deceased who is not also the surviving spouse's child. A särkullbarn does not have to wait. They can take their share directly. They can also choose to wait and receive efterarv instead (3 kap. 9 §).

So the result has two lists. arvingar is who inherits now and always sums to 1. efterarvingar is who inherits from the first deceased's share when the spouse dies, expressed as shares of that future part:

const sarkull = grenar.filter((g) => !g.barn.gemensamt);
const avstar = !!input.sarkullbarnAvstar && sarkull.length > 0;
const nu: Arvinge[] = [];
const efter: Arvinge[] = [];
for (const line of klass1) {
  const n = Number(line.key.split('-').pop());
  const g = grenar.find((x) => x.index + 1 === n)!;
  if (g.barn.gemensamt || avstar) efter.push(line);
  else nu.push(line);
}
const makeDel = sub(ONE, sum(nu));

(gemensamt means "shared child", nu is "now", efter is "after", make is spouse, avstar is "waives".)

Notice the order of operations. The first class is computed exactly as if there were no spouse. Only then are branches sorted into "now" and "later". The spouse takes 1 - sum(nu). That is how 3 kap. 1 § is written: the spouse gets everything except the lots of särkullbarn who have not waived.

The deferred list is renormalised so that it reads as shares of the future part:

const efterSum = sum(efter);
const inv = isZero(efterSum) ? ONE : frac(efterSum.d, efterSum.n);
const efterNorm = isZero(efterSum) ? [] : scale(efter, inv);

With one shared child and one särkullbarn, the särkullbarn takes 1/2 now, the spouse takes 1/2, and the shared child is listed as taking 1/1 of the efterarv. This follows 3 kap. 2 § 2 st, which reduces a child's efterarv by whatever they already took out at the first death.

With no children at all, the spouse takes everything, and the second-class relatives (parents, siblings, their descendants) become the efterarv heirs. Grandparents and aunts do not, because 3 kap. 2 § lists only descendants, parents, siblings and siblings' descendants. If none exist, 3 kap. 8 § says the spouse's own heirs take everything later.

One more rule never shows up as a fraction: the spouse's floor. 3 kap. 1 § 2 st guarantees the spouse property worth four prisbasbelopp (a yearly indexed amount, 59,200 SEK for 2026), counted together with what they already own. It overrides särkullbarn and even a will. The engine cannot compute it without asset values, so it is emitted as a warning with the 2026 amount, not as a share.

Refusing is a feature

The result has a field most calculators do not have:

/** Branches the tool does not compute. Shown as "kontakta jurist". */
jurist: string[];

"Kontakta jurist" means "contact a lawyer". Every result carries a list of situations the tool deliberately does not handle:

  • there is a will, an inheritance contract or a prenup with special terms
  • the deceased did not live in Sweden, or there are assets abroad (another country's law may apply)
  • a grandchild or niece or nephew has also died and left children (the tool computes one generation of representation, not more)
  • a divorce case was pending at the death, in which case the spouse does not inherit at all (3 kap. 10 §)
  • in the third class, a parent of the deceased had half siblings

None of these are hard to code in isolation. Deeper representation is a recursion. The reason to refuse is about how the tool is used. A person typing their family into a free calculator usually does not know that a pending divorce case changes the answer, or that a house in Spain may bring Spanish law with it. A confident 1/4 with no caveat is worse than no answer, because people act on it.

So the scope is narrow on purpose, and the boundary is stated in the output, every time, next to the numbers. Each extra case would be one more silent assumption about the user's family. The tool says out loud what it did not do instead.

Testing it as a table of families

The test file is a list of families, each with the expected shares written as strings. A helper turns a result into a key -> "n/d" map, so a failing assertion shows the fractions directly:

const shares = (r: ArvResult, list: 'arvingar' | 'efterarvingar' = 'arvingar') =>
  Object.fromEntries(r[list].map((a) => [a.key, `${a.var.n}/${a.var.d}`]));

test('avlidet barn med två barnbarn: istadarätt per gren', () => {
  const r = run({ barn: [alive(), alive(), dead(2)] });
  assert.deepEqual(shares(r), { 'barn-1': '1/3', 'barn-2': '1/3', 'barnbarn-3': '1/6' });
});

test('båda föräldrarna döda, ett helsyskon och ett halvsyskon på mammas sida', () => {
  const r = run({ syskon: [{ typ: 'hel', lever: true, barn: 0 },
                           { typ: 'halvMor', lever: true, barn: 0 }] });
  assert.deepEqual(shares(r), { 'syskon-1': '3/4', 'syskon-2': '1/4' });
});

test('gift med ett gemensamt barn och ett särkullbarn', () => {
  const r = run({ civil: 'gift', barn: [alive(true), alive(false)] });
  assert.deepEqual(shares(r), { make: '1/2', 'barn-2': '1/2' });
  assert.deepEqual(shares(r, 'efterarvingar'), { 'barn-1': '1/1' });
});

There are 23 of these, one or more per statute paragraph, including the cases where a relative gets nothing (a half sibling on the side of the living parent, grandparents when there is a spouse, a cohabiting partner, who is not in the order of succession at all). Each expected value was worked out by hand from the statute text before the test ran. The test table is the spec, written in the law's own units.

On top of the table there is one invariant test. It runs messy families through the engine and checks that the shares sum to exactly 1:

for (const c of cases) assert.equal(fmtFrac(total(run(c))), 'allt', JSON.stringify(c));

fmtFrac returns "allt" ("everything") only for exactly 1/1. With exact fractions this is a real equality check. With floats it would be an epsilon check, and that is the test that would let a lost 1/12 slip through.

What I would tell someone modelling another country's rules

  • Draw the family as branches and divide by live branches, never by heads.
  • Resolve each half (or quarter) of the estate on its own, then merge by stable key. Half siblings and empty sides stop being special cases.
  • Keep the statute citation on every output line. It turns a number into a claim someone can check.
  • Use exact fractions. The sum-to-one invariant is your best test, and it only works without rounding.
  • Put the refused cases in the output, not in a FAQ. A list of what the tool did not compute is part of the answer.

The code above runs the free calculator at vittnet.nu/verktyg/vem-arver, which I built on this engine.

Comments (0)

Sign in to join the discussion

Be the first to comment!