export interface Removed { item: T; index: number; } /** In place; `undefined` means nothing matched and the list is untouched. */ export function removeFirst(list: T[], predicate: (item: T) => boolean): Removed | undefined { const index = list.findIndex(predicate); // A valid index makes `splice` return exactly one element, which is what the `!` rests on. return index < 0 ? undefined : { item: list.splice(index, 1)[0]!, index }; } /** In place; the returned indices are the former ones, ascending. */ export function removeWhere(list: T[], predicate: (item: T) => boolean): Removed[] { const removed: Removed[] = []; list.forEach((item, index) => { if (predicate(item)) removed.push({ item, index }); }); for (let i = removed.length - 1; i >= 0; i--) list.splice(removed[i]!.index, 1); return removed; } /** Inserting in ascending index order is what restores the list exactly. */ export function restoreAt(list: T[], removed: readonly Removed[]): void { for (const entry of [...removed].sort((a, b) => a.index - b.index)) list.splice(entry.index, 0, entry.item); } /** Returns a new list; unlike the helpers above this one does not mutate. */ export function removeById(list: readonly T[], id: string): T[] { return list.filter((item) => item.id !== id); }