Skip to content
This repository was archived by the owner on Aug 10, 2026. It is now read-only.

Releases: noshiro-pf/ts-data-forge

ts-data-forge@14.2.0

Choose a tag to compare

@noshiro-repo-automation-bot noshiro-repo-automation-bot released this 10 Aug 09:22
Immutable release. Only release title and notes can be modified.
07fd061

Minor Changes

  • 695411a: Make the curried Optional.map, Result.map and Result.mapErr usable from
    a caller that is itself generic over the container.

    Each curried overload used to fix its parameter to the container spelled in
    terms of the mapper's input — (optional: Optional<S>) => …,
    <E>(result: Result<S, E>) => …, <S>(result: Result<S, E>) => …. Because
    S (resp. E) is bound when the mapper is passed, a caller generic over the
    container ends up needing O to be assignable to Optional<Unwrap<O>>, which
    TypeScript cannot show for a bare O extends UnknownOptional — even though
    every concrete instantiation satisfies it. The curried form was therefore
    unreachable from such a caller, which had to fall back to the direct, two-argument
    Optional.map(a, mapFn).

    The returned function is now generic over the whole container, mirroring the
    direct overload:

    export function map<S, S2>(
        mapFn: (value: S) => S2,
    ): <O extends UnknownOptional>(
        optional: Unwrap<O> extends S ? O : never,
    ) => Optional<S2>;

    The conditional preserves the check — mapping a (s: string) => … over an
    Optional<number> is still an error — at the cost of a worse message for that
    case (not assignable to 'never' rather than naming the two optionals).
    Ordinary curried usage is unaffected and still infers the result precisely.

eslint-plugin-ts-data-forge@0.4.3

Choose a tag to compare

@noshiro-repo-automation-bot noshiro-repo-automation-bot released this 10 Aug 09:22
Immutable release. Only release title and notes can be modified.
07fd061

Patch Changes

  • Updated dependencies [695411a]
    • ts-data-forge@14.2.0

ts-data-forge@14.1.0

Choose a tag to compare

@noshiro-repo-automation-bot noshiro-repo-automation-bot released this 09 Aug 11:25
Immutable release. Only release title and notes can be modified.
29db9b7

Minor Changes

  • f9f6b08: Arr.every used as a type guard now narrows to the element-substituted array
    rather than to a bare readonly S[], so a tuple stays a tuple of S and a
    length-constrained array keeps its brand:

    declare const xs: MinLengthArray<3, string | number>;
    
    if (Arr.every(xs, isString)) {
        // now assignable to MinLengthArray<3, string>; previously it was not
        takesNonEmptyStrings(xs);
    }
    
    declare const pair: FixedLengthTuple<2, string | number>;
    
    if (Arr.every(pair, isString)) {
        // now assignable to readonly [string, string]; previously it was not
        takesStringPair(pair);
    }

    The old predicate said array is readonly S[]. TypeScript intersected that
    with the declared type, so indexed access happened to come out right, but the
    narrowed type was not assignable to the same container with S elements —
    callers had to reach for a type assertion to pass it on.

    Brand-carrying arrays select a separate overload returning
    ChangeArrayElement<Ar, S> & Ar; everything else states the homomorphic
    mapping directly, for the same reason as in Arr.map — a generic Ar
    cannot decide HasLengthConstraint. Both are intersected with the input, so a
    brand intersected with an exact tuple — the shape Arr.isMinLengthArray and
    Arr.asMinLengthArray produce — keeps the brand, the length and the positions
    all at once.

    The curried form gets the same treatment. Its two cases are overloads of the
    returned guard rather than of Arr.every itself, so a single
    Arr.every(predicate) value still accepts branded and unbranded arrays alike.

    Arr.every with a plain boolean predicate, and Arr.some, are unchanged.

Patch Changes

  • 20c6d36: Arr.toSorted is usable from a function that is itself generic over the array.
    Its parameter list was a conditional type — a tuple with an optional comparator
    for readonly number[] and a required one otherwise — and a generic Ar cannot
    decide it, so the whole argument list was rejected:

    const sortAscending = <const T extends readonly number[]>(
        xs: T,
    ): readonly T[number][] => Arr.toSorted(xs, (a, b) => a - b);
    // Argument of type '[T, (a: number, b: number) => number]' is not assignable
    // to parameter of type 'T extends readonly number[] ? ... : ...'

    The two cases are now two overloads, so resolution picks one per call. Concrete
    callers are unaffected — the optional comparator for numbers, the required one
    for everything else, and every reported result type are unchanged.

    This is the same class of problem 14.0.1 fixed for Arr.map, Arr.toFilled
    and Arr.toRangeFilled, except on the parameter side rather than the return
    side.

    Arr.tail, Arr.butLast, Arr.zip, Arr.set and Arr.toUpdated still do not
    resolve under a generic array parameter. They report ConstrainedList.Tail /
    Zip / SetAt, whose brand branch computes on the input's own bounds, and that
    computation expands the whole SupportedLength union when the input is a type
    parameter. Fixing those needs a change in ts-type-forge, not here; the reason is
    recorded in array-utils-shape-invariants.test.mts alongside the coverage.

eslint-plugin-ts-data-forge@0.4.2

Choose a tag to compare

@noshiro-repo-automation-bot noshiro-repo-automation-bot released this 09 Aug 11:25
Immutable release. Only release title and notes can be modified.
29db9b7

Patch Changes

  • Updated dependencies [f9f6b08]
  • Updated dependencies [20c6d36]
    • ts-data-forge@14.1.0

ts-data-forge@14.0.1

Choose a tag to compare

released this 05 Aug 20:48
Immutable release. Only release title and notes can be modified.
c8b8a1b

Patch Changes

  • c3cbdbd: Arr.map, Arr.toFilled and Arr.toRangeFilled again report the plain
    homomorphic mapping ({ [K in keyof Ar]: B }) for an array or tuple that
    carries no length brand, so transforming a tuple into a same-length tuple
    works inside a function that is itself generic over the tuple:

    const mapValues = <const T extends readonly Readonly<{ v: unknown }>[]>(
        boxes: T,
    ): Readonly<{ [K in keyof T]: unknown }> => Arr.map(boxes, (b) => b.v);

    Since v14 the return type was a conditional branching on
    HasLengthConstraint<Ar>, which a generic Ar cannot decide. The
    conditional stayed deferred and its branded branch made the result
    unassignable to the caller's own mapping, so callers had to reach for a type
    assertion. Concrete tuples were unaffected and keep the same result as before.

    Brand-carrying arrays are unaffected too: they select a separate overload that
    still returns ChangeArrayElement<Ar, …>, so Arr.map on a
    MinLengthArray<2, number> still yields a MinLengthArray<2, …>.

    Arr.toSorted, Arr.toSortedBy, Arr.toReversed, Arr.toUpdated,
    Arr.tail, Arr.butLast and Arr.zip still do not resolve under a generic
    array parameter and are unchanged here.

eslint-plugin-ts-data-forge@0.4.1

Choose a tag to compare

released this 05 Aug 20:48
Immutable release. Only release title and notes can be modified.
c8b8a1b

Patch Changes

  • Updated dependencies [c3cbdbd]
    • ts-data-forge@14.0.1

eslint-plugin-ts-data-forge@0.4.0

Choose a tag to compare

released this 05 Aug 15:12
Immutable release. Only release title and notes can be modified.
c9b631f

Minor Changes

  • 0dcdcbc: prefer-is-record-and-has-key now drops the isRecord(...) conjunct when the
    object already satisfies hasKey's R extends UnknownRecord constraint, so
    Object.hasOwn(record, key) on a record-typed value rewrites to
    hasKey(record, key) instead of isRecord(record) && hasKey(record, key).

    The check needs type information; without it the guard is kept, as before. It
    is deliberately conservative — a type TypeScript would accept through an
    implicit index signature keeps the guard — and callables and arrays keep it
    too, because isRecord rejects those at runtime.

    no-unnecessary-type-guard recognizes isRecord for the same reason: it now
    reports isRecord(x) as always true when every union member already
    satisfies UnknownRecord, and as always false when none of them can (every
    primitive, array, tuple and callable).

Patch Changes

  • 938bc58: Widen the @typescript-eslint/utils dependency to ^8.65.0 and pin
    ts-type-forge to ~9.1.1.

ts-data-forge@14.0.0

Choose a tag to compare

released this 04 Aug 17:41
Immutable release. Only release title and notes can be modified.
489ca61

Major Changes

  • 4dfe5d9: The Arr and Str length-constrained guards and casts now take their length
    arguments first
    , matching the type-parameter order of the ts-type-forge types
    they narrow to, and each Arr one gained a curried form.

    before after
    Arr.isMinLengthArray(xs, 3) Arr.isMinLengthArray(3, xs)
    Arr.isMaxLengthArray(xs, 8) Arr.isMaxLengthArray(8, xs)
    Arr.isFixedLengthArray(xs, 3) Arr.isFixedLengthArray(3, xs)
    Arr.isBoundedLengthArray(xs, 1, 5) Arr.isBoundedLengthArray(1, 5, xs)
    Arr.isFixedLengthTuple(xs, 2) Arr.isFixedLengthTuple(2, xs)
    Arr.isMinLengthTuple(xs, 1) Arr.isMinLengthTuple(1, xs)
    Arr.isMaxLengthTuple(xs, 2) Arr.isMaxLengthTuple(2, xs)
    Arr.isBoundedLengthTuple(xs, 1, 3) Arr.isBoundedLengthTuple(1, 3, xs)
    Arr.asMinLengthArray(xs, 3) Arr.asMinLengthArray(3, xs)
    Arr.asMaxLengthArray(xs, 8) Arr.asMaxLengthArray(8, xs)
    Arr.asFixedLengthArray(xs, 3) Arr.asFixedLengthArray(3, xs)
    Arr.asBoundedLengthArray(xs, 1, 5) Arr.asBoundedLengthArray(1, 5, xs)

    Str gets the same treatment, so the two namespaces read alike:

    before after
    Str.isMinLengthString(s, 12) Str.isMinLengthString(12, s)
    Str.isMaxLengthString(s, 32) Str.isMaxLengthString(32, s)
    Str.isFixedLengthString(s, 2) Str.isFixedLengthString(2, s)
    Str.isBoundedLengthString(s, 8, 16) Str.isBoundedLengthString(8, 16, s)
    Str.asMinLengthString(s, 12) Str.asMinLengthString(12, s)
    Str.asMaxLengthString(s, 32) Str.asMaxLengthString(32, s)
    Str.asFixedLengthString(s, 2) Str.asFixedLengthString(2, s)
    Str.asBoundedLengthString(s, 8, 16) Str.asBoundedLengthString(8, 16, s)

    The type parameters follow the same order — the container (Xs / S / E)
    now comes after the lengths, e.g. Arr.asMinLengthArray<Xs, MinLength> is now
    Arr.asMinLengthArray<MinLength, Xs> — so an explicit type argument list reads
    in the same order as the call.

    Omitting the array or string now returns a reusable guard/cast, so the curried
    form is plain partial application of the same signature:

    const hasThree = Arr.isMinLengthArray(3);
    const isSmallSelection = Arr.isBoundedLengthArray(1, 5);
    const asRgb = Arr.asFixedLengthArray(3);

    The eight Arr.is* guards gained curried overloads; the as* casts in both
    namespaces already had one, and its argument order is unchanged.

    BREAKING CHANGE: every call to the twenty functions above must swap its
    arguments so the length comes first, and an explicit type argument list must
    move the container type last. The curried as* overloads are unaffected.

    Length-preserving array utilities now keep the branded length constraint of
    their input.
    Arr.map was outright broken for a length-branded input:
    MinLengthArray<3, E> is an intersection of a tuple and the brand object, which
    TypeScript does not treat as an array type, so the homomorphic
    Readonly<{ [K in keyof Ar]: B }> mapped length, the array methods and the
    brand keys to B as well and produced a non-array object. It now goes through
    ts-type-forge's ChangeArrayElement, which rebuilds the structural part from the
    bounds recovered from the brand and re-applies the brand:

    declare const history: MinLengthArray<3, number>;
    
    const labels = Arr.map(history, (n) => `#${n}`); // MinLengthArray<3, string>
    const first: string = labels[0]; // no `undefined`, as before the map

    Arr.toSorted, Arr.toSortedBy, Arr.toReversed, Arr.set, Arr.toUpdated,
    Arr.toFilled and Arr.toRangeFilled previously degraded a branded input to
    NonEmptyArray (or a plain array) and now keep the full constraint too.
    (Arr.copy already returned its input type, so it was never affected.)

    Arr.partition / Arr.chunk now report the bounds of each chunk, and
    Arr.create / Arr.newArray report an exact length past the tuple range:

    call before after
    Arr.partition(xs, 2) readonly E[][] readonly BoundedLengthArray<1, 2, E>[]
    Arr.create(12, 'x') NonEmptyArray<'x'> FixedLengthArray<12, 'x'>

    The chunk's lower bound is the point: partition never emits an empty chunk,
    so indexed access into one is no longer undefined at position 0.

    const [firstChunk] = Arr.partition(values, 2);
    
    if (firstChunk !== undefined) {
        const first = firstChunk[0]; // not `| undefined`
    }

    BREAKING CHANGE: a chunk is now branded, so an expected value compared against
    one needs the unbranded type spelled out — assert.deepStrictEqual<readonly (readonly number[])[]>(chunks, […]).
    Arr.zeros and Arr.seq are deliberately unchanged: their argument type only
    carries literals up to SmallUint, which they already expand into an exact
    tuple, so there is no case left for a brand to describe.

    The array operations now report the right length constraint for a
    length-constrained input
    , via ts-type-forge's ConstrainedList.

    Arr.set, Arr.toUpdated, Arr.tail, Arr.butLast, Arr.toReversed and
    Arr.zip were annotated with List.*, which decides what it can say from
    whether the input is a fixed-length tuple. A length-constrained array is not one
    — its length is number — so List handed the input type straight back. For
    the operations that shorten an array that is not merely imprecise, it is
    wrong:

    declare const xs: MinLengthArray<5, string>;
    
    Arr.tail(xs);
    // before: MinLengthArray<5, string> — a 4-element result claiming at least five
    // after:  MinLengthArray<4, string>

    Arr.set and Arr.toUpdated gain positional precision from the same change:

    Arr.set([1, 2, 3] as const, 1, 'x');
    // before: readonly [1 | 'x', 2 | 'x', 3 | 'x']
    // after:  readonly [1, 'x', 3]

    Arr.toReversed also drops a hand-rolled workaround
    (HasLengthConstraint<Ar> extends true ? ChangeArrayElement<Ar, Ar[number]> : List.Reverse<Ar>)
    which kept the length but collapsed the element positions to their union. One
    annotation now covers plain tuples and branded arrays alike.

    A brand intersected with an exact tuple — the shape Arr.isMinLengthArray(3, xs)
    produces — keeps all three of the brand, the exact length and the positions.

    A union index is handled soundly, upstream. ArgArrayIndex<Ar> for a tuple
    is the union of its indices, so this is reachable from ordinary code:

    declare const i: 0 | 2;
    Arr.set([1, 2, 3] as const, i, 'x');

    The call replaces one position or the other and never both, so a positional
    answer of readonly ['x', 2, 'x'] is a type that no possible result satisfies.
    The cause was upstream: List.SetAt walked the tuple asking Position extends I,
    which every member of a union answers at once, so all of them were replaced.
    ts-type-forge 9.1.1 fixes that at the source, so Arr.set and Arr.toUpdated
    pass the index straight through. Upstream widens only the positions the index can
    actually name:

    Arr.set([1, 2, 3] as const, i, 'x'); // i: 0 | 2
    // readonly [1 | 'x', 2, 3 | 'x'] — index 1 is not among `0 | 2`, so it stays `2`

    A single literal index keeps the exact positional answer, as before.

    Arr.take, Arr.takeLast, Arr.skip and Arr.skipLast are sound now, but
    not more precise.
    They used to hand a branded input straight back —
    Arr.take(xs, 2) on an "at least 5" array claimed the two-element result still
    held at least five. They now return the unconstrained readonly E[] for a
    branded input, and keep the exact List.* tuple result for a plain array or
    tuple.

    They do not yet use ConstrainedList.Take and friends. Against ts-type-forge
    9.1.0 they could not: those rebuilt the bounds from N and could not be
    instantiated against a still-generic Ar at all. 9.1.1 lifts that — it no
    longer distributes over N, so the instantiation stays bounded — which makes
    propagating the bound through these four possible as a follow-up. It is left out
    of this release because it changes their result type for branded inputs and
    deserves its own measurement.

    Arr.toSorted and Arr.toSortedBy deliberately keep their existing
    annotation.
    An arbitrary permutation may move any element anywhere, so "same
    length, element type widened to the union" stays the honest answer for them.

    Type-parameter order now follows value-parameter order everywhere. An audit
    comparing each type parameter's declaration position against the first value
    parameter that mentions it turned up two more signatures out of step, both
    reordered:

    function before after
    Arr.sumBy(array, mapFn) <N, E> <E, N>
    `Arr.partition(array, c...
Read more

ts-data-forge@13.0.0

Choose a tag to compare

released this 04 Aug 11:14
Immutable release. Only release title and notes can be modified.
492ce41

Major Changes

  • 534ac4d: Arr.isEmpty and Arr.isNonEmpty now narrow to the branded length-constrained
    array types
    , matching the rest of the Arr length-guard family.

    guard before after
    Arr.isEmpty(xs) readonly [] FixedLengthArray<0, E> & Xs
    Arr.isNonEmpty(xs) NonEmptyArray<E> MinLengthArray<1, E> & Xs

    isEmpty was missing the brand entirely, so it was not equivalent to
    Arr.isFixedLengthArray(xs, 0); isNonEmpty had the brand but dropped the input
    type instead of intersecting with it the way Arr.isMinLengthArray does. Both
    now behave exactly like their is*LengthArray counterparts.

    BREAKING CHANGE: the narrowed types are strictly narrower than before. Code that
    reads the narrowed value keeps compiling, but an explicit annotation such as
    const empty: readonly [] = xs after the guard, or passing the narrowed value
    where an unbranded array literal is expected, may now need the unbranded type
    spelled out.

    prefer-canonical-length-guard follows the new semantics: isFixedLengthArray(xs, 0)
    and isMinLengthArray(xs, 1) are now the type-identical rewrites, and the
    structural *Tuple guards (isFixedLengthTuple(xs, 0), isMaxLengthTuple(xs, 0),
    isBoundedLengthTuple(xs, 0, 0), isMinLengthTuple(xs, 1)) are rewritten too —
    those strengthen the narrowed type by adding the brand.

    prefer-canonical-length-guard additionally absorbs the five xs.length <op> n
    comparison rules — prefer-arr-is-non-empty, prefer-arr-is-min-length-array,
    prefer-arr-is-max-length-array, prefer-arr-is-bounded-length-array and
    prefer-arr-is-fixed-length-array — so one rule now covers both
    comparison → guard and guard → guard normalization.

    BREAKING CHANGE: those five rule names are removed from the plugin; enable
    ts-data-forge/prefer-canonical-length-guard instead. Their behavior is
    unchanged — the rule reuses their implementations rather than reimplementing
    them.

eslint-plugin-ts-data-forge@0.3.0

Choose a tag to compare

released this 04 Aug 17:41
Immutable release. Only release title and notes can be modified.
489ca61

Minor Changes

  • 4dfe5d9: The Arr and Str length-constrained guards and casts now take their length
    arguments first
    , matching the type-parameter order of the ts-type-forge types
    they narrow to, and each Arr one gained a curried form.

    before after
    Arr.isMinLengthArray(xs, 3) Arr.isMinLengthArray(3, xs)
    Arr.isMaxLengthArray(xs, 8) Arr.isMaxLengthArray(8, xs)
    Arr.isFixedLengthArray(xs, 3) Arr.isFixedLengthArray(3, xs)
    Arr.isBoundedLengthArray(xs, 1, 5) Arr.isBoundedLengthArray(1, 5, xs)
    Arr.isFixedLengthTuple(xs, 2) Arr.isFixedLengthTuple(2, xs)
    Arr.isMinLengthTuple(xs, 1) Arr.isMinLengthTuple(1, xs)
    Arr.isMaxLengthTuple(xs, 2) Arr.isMaxLengthTuple(2, xs)
    Arr.isBoundedLengthTuple(xs, 1, 3) Arr.isBoundedLengthTuple(1, 3, xs)
    Arr.asMinLengthArray(xs, 3) Arr.asMinLengthArray(3, xs)
    Arr.asMaxLengthArray(xs, 8) Arr.asMaxLengthArray(8, xs)
    Arr.asFixedLengthArray(xs, 3) Arr.asFixedLengthArray(3, xs)
    Arr.asBoundedLengthArray(xs, 1, 5) Arr.asBoundedLengthArray(1, 5, xs)

    Str gets the same treatment, so the two namespaces read alike:

    before after
    Str.isMinLengthString(s, 12) Str.isMinLengthString(12, s)
    Str.isMaxLengthString(s, 32) Str.isMaxLengthString(32, s)
    Str.isFixedLengthString(s, 2) Str.isFixedLengthString(2, s)
    Str.isBoundedLengthString(s, 8, 16) Str.isBoundedLengthString(8, 16, s)
    Str.asMinLengthString(s, 12) Str.asMinLengthString(12, s)
    Str.asMaxLengthString(s, 32) Str.asMaxLengthString(32, s)
    Str.asFixedLengthString(s, 2) Str.asFixedLengthString(2, s)
    Str.asBoundedLengthString(s, 8, 16) Str.asBoundedLengthString(8, 16, s)

    The type parameters follow the same order — the container (Xs / S / E)
    now comes after the lengths, e.g. Arr.asMinLengthArray<Xs, MinLength> is now
    Arr.asMinLengthArray<MinLength, Xs> — so an explicit type argument list reads
    in the same order as the call.

    Omitting the array or string now returns a reusable guard/cast, so the curried
    form is plain partial application of the same signature:

    const hasThree = Arr.isMinLengthArray(3);
    const isSmallSelection = Arr.isBoundedLengthArray(1, 5);
    const asRgb = Arr.asFixedLengthArray(3);

    The eight Arr.is* guards gained curried overloads; the as* casts in both
    namespaces already had one, and its argument order is unchanged.

    BREAKING CHANGE: every call to the twenty functions above must swap its
    arguments so the length comes first, and an explicit type argument list must
    move the container type last. The curried as* overloads are unaffected.

    Length-preserving array utilities now keep the branded length constraint of
    their input.
    Arr.map was outright broken for a length-branded input:
    MinLengthArray<3, E> is an intersection of a tuple and the brand object, which
    TypeScript does not treat as an array type, so the homomorphic
    Readonly<{ [K in keyof Ar]: B }> mapped length, the array methods and the
    brand keys to B as well and produced a non-array object. It now goes through
    ts-type-forge's ChangeArrayElement, which rebuilds the structural part from the
    bounds recovered from the brand and re-applies the brand:

    declare const history: MinLengthArray<3, number>;
    
    const labels = Arr.map(history, (n) => `#${n}`); // MinLengthArray<3, string>
    const first: string = labels[0]; // no `undefined`, as before the map

    Arr.toSorted, Arr.toSortedBy, Arr.toReversed, Arr.set, Arr.toUpdated,
    Arr.toFilled and Arr.toRangeFilled previously degraded a branded input to
    NonEmptyArray (or a plain array) and now keep the full constraint too.
    (Arr.copy already returned its input type, so it was never affected.)

    Arr.partition / Arr.chunk now report the bounds of each chunk, and
    Arr.create / Arr.newArray report an exact length past the tuple range:

    call before after
    Arr.partition(xs, 2) readonly E[][] readonly BoundedLengthArray<1, 2, E>[]
    Arr.create(12, 'x') NonEmptyArray<'x'> FixedLengthArray<12, 'x'>

    The chunk's lower bound is the point: partition never emits an empty chunk,
    so indexed access into one is no longer undefined at position 0.

    const [firstChunk] = Arr.partition(values, 2);
    
    if (firstChunk !== undefined) {
        const first = firstChunk[0]; // not `| undefined`
    }

    BREAKING CHANGE: a chunk is now branded, so an expected value compared against
    one needs the unbranded type spelled out — assert.deepStrictEqual<readonly (readonly number[])[]>(chunks, […]).
    Arr.zeros and Arr.seq are deliberately unchanged: their argument type only
    carries literals up to SmallUint, which they already expand into an exact
    tuple, so there is no case left for a brand to describe.

    The array operations now report the right length constraint for a
    length-constrained input
    , via ts-type-forge's ConstrainedList.

    Arr.set, Arr.toUpdated, Arr.tail, Arr.butLast, Arr.toReversed and
    Arr.zip were annotated with List.*, which decides what it can say from
    whether the input is a fixed-length tuple. A length-constrained array is not one
    — its length is number — so List handed the input type straight back. For
    the operations that shorten an array that is not merely imprecise, it is
    wrong:

    declare const xs: MinLengthArray<5, string>;
    
    Arr.tail(xs);
    // before: MinLengthArray<5, string> — a 4-element result claiming at least five
    // after:  MinLengthArray<4, string>

    Arr.set and Arr.toUpdated gain positional precision from the same change:

    Arr.set([1, 2, 3] as const, 1, 'x');
    // before: readonly [1 | 'x', 2 | 'x', 3 | 'x']
    // after:  readonly [1, 'x', 3]

    Arr.toReversed also drops a hand-rolled workaround
    (HasLengthConstraint<Ar> extends true ? ChangeArrayElement<Ar, Ar[number]> : List.Reverse<Ar>)
    which kept the length but collapsed the element positions to their union. One
    annotation now covers plain tuples and branded arrays alike.

    A brand intersected with an exact tuple — the shape Arr.isMinLengthArray(3, xs)
    produces — keeps all three of the brand, the exact length and the positions.

    A union index is handled soundly, upstream. ArgArrayIndex<Ar> for a tuple
    is the union of its indices, so this is reachable from ordinary code:

    declare const i: 0 | 2;
    Arr.set([1, 2, 3] as const, i, 'x');

    The call replaces one position or the other and never both, so a positional
    answer of readonly ['x', 2, 'x'] is a type that no possible result satisfies.
    The cause was upstream: List.SetAt walked the tuple asking Position extends I,
    which every member of a union answers at once, so all of them were replaced.
    ts-type-forge 9.1.1 fixes that at the source, so Arr.set and Arr.toUpdated
    pass the index straight through. Upstream widens only the positions the index can
    actually name:

    Arr.set([1, 2, 3] as const, i, 'x'); // i: 0 | 2
    // readonly [1 | 'x', 2, 3 | 'x'] — index 1 is not among `0 | 2`, so it stays `2`

    A single literal index keeps the exact positional answer, as before.

    Arr.take, Arr.takeLast, Arr.skip and Arr.skipLast are sound now, but
    not more precise.
    They used to hand a branded input straight back —
    Arr.take(xs, 2) on an "at least 5" array claimed the two-element result still
    held at least five. They now return the unconstrained readonly E[] for a
    branded input, and keep the exact List.* tuple result for a plain array or
    tuple.

    They do not yet use ConstrainedList.Take and friends. Against ts-type-forge
    9.1.0 they could not: those rebuilt the bounds from N and could not be
    instantiated against a still-generic Ar at all. 9.1.1 lifts that — it no
    longer distributes over N, so the instantiation stays bounded — which makes
    propagating the bound through these four possible as a follow-up. It is left out
    of this release because it changes their result type for branded inputs and
    deserves its own measurement.

    Arr.toSorted and Arr.toSortedBy deliberately keep their existing
    annotation.
    An arbitrary permutation may move any element anywhere, so "same
    length, element type widened to the union" stays the honest answer for them.

    Type-parameter order now follows value-parameter order everywhere. An audit
    comparing each type parameter's declaration position against the first value
    parameter that mentions it turned up two more signatures out of step, both
    reordered:

    function before after
    Arr.sumBy(array, mapFn) <N, E> <E, N>
    `Arr.partition(array, c...
Read more