refactor: add successOr, sweep remaining inline unwraps (RD-17)

Eight sites hand-rolled `rd.tag === 'Success' ? rd.value : fallback`. Six
take the new `successOr(rd, fallback)`, one takes the existing `successOf`,
and one (`big-profile.store.ts`) uses the existing `map`, since it returns a
RemoteData rather than an unwrapped value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-04 20:33:56 +02:00
co-authored by Claude Sonnet 5
parent c36d9e3ff0
commit e221834f6e
13 changed files with 212 additions and 37 deletions
@@ -1,5 +1,5 @@
import { Injectable, computed, inject, signal } from '@angular/core';
import { RemoteData } from '@shared/application/remote-data';
import { RemoteData, successOr } from '@shared/application/remote-data';
import { runSubmit } from '@shared/application/submit';
import { Result, ok, err } from '@shared/kernel/fp';
import { FeatureFlag } from '@shared/domain/feature-flag';
@@ -22,10 +22,7 @@ export class FeatureFlagStore {
readonly flags = this.state.asReadonly();
/** The resolved list (empty until loaded) — for the admin toggle UI. */
readonly all = computed(() => {
const rd = this.state();
return rd.tag === 'Success' ? rd.value : [];
});
readonly all = computed(() => successOr(this.state(), []));
constructor() {
void this.load();
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { RemoteData, fromLoadLifecycle, map2, map, successOf } from './remote-data';
import { RemoteData, fromLoadLifecycle, map2, map, successOf, successOr } from './remote-data';
import { loading, failure, empty, success } from '../testing/remote-data';
const loadingRd: RemoteData<string, number> = loading();
@@ -34,6 +34,18 @@ describe('successOf', () => {
});
});
describe('successOr', () => {
it('unwraps a Success value', () => {
expect(successOr(ok(2), 0)).toBe(2);
});
it('is the fallback for every other state', () => {
expect(successOr(loadingRd, [])).toEqual([]);
expect(successOr(failureRd, [])).toEqual([]);
expect(successOr(empty(), null)).toBeNull();
});
});
describe('fromLoadLifecycle', () => {
it('maps Loading → Loading', () => {
expect(fromLoadLifecycle({ tag: 'Loading' })).toEqual(loading());
@@ -109,3 +109,13 @@ export function andThen<E, A, B>(
export function successOf<E, T>(rd: RemoteData<E, T>): T | undefined {
return rd.tag === 'Success' ? rd.value : undefined;
}
/** Unwrap a Success value, or a caller-supplied `fallback` for every other state.
Reach for this over `successOf` when `undefined` is not a usable value at the
call site (e.g. a list the template iterates, which wants `[]`). Reach for
`map` instead when the site needs to stay a `RemoteData`, not an unwrapped
value. Three type parameters: `fallback` need not be assignable to `T`
(an empty array is not a `T[]` at the type level, only at the value level). */
export function successOr<E, T, F>(rd: RemoteData<E, T>, fallback: F): T | F {
return rd.tag === 'Success' ? rd.value : fallback;
}