ts-data-forge@14.0.0
Major Changes
-
4dfe5d9: The
ArrandStrlength-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 eachArrone 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)Strgets 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; theas*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 curriedas*overloads are unaffected.Length-preserving array utilities now keep the branded length constraint of
their input.Arr.mapwas 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 }>mappedlength, the array methods and the
brand keys toBas well and produced a non-array object. It now goes through
ts-type-forge'sChangeArrayElement, 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.toFilledandArr.toRangeFilledpreviously degraded a branded input to
NonEmptyArray(or a plain array) and now keep the full constraint too.
(Arr.copyalready returned its input type, so it was never affected.)Arr.partition/Arr.chunknow report the bounds of each chunk, and
Arr.create/Arr.newArrayreport 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:
partitionnever emits an empty chunk,
so indexed access into one is no longerundefinedat position0.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.zerosandArr.seqare deliberately unchanged: their argument type only
carries literals up toSmallUint, 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'sConstrainedList.Arr.set,Arr.toUpdated,Arr.tail,Arr.butLast,Arr.toReversedand
Arr.zipwere annotated withList.*, which decides what it can say from
whether the input is a fixed-length tuple. A length-constrained array is not one
— itslengthisnumber— soListhanded 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.setandArr.toUpdatedgain 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.toReversedalso 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 ofreadonly ['x', 2, 'x']is a type that no possible result satisfies.
The cause was upstream:List.SetAtwalked the tuple askingPosition 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, soArr.setandArr.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.skipandArr.skipLastare 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 unconstrainedreadonly E[]for a
branded input, and keep the exactList.*tuple result for a plain array or
tuple.They do not yet use
ConstrainedList.Takeand friends. Against ts-type-forge
9.1.0 they could not: those rebuilt the bounds fromNand could not be
instantiated against a still-genericArat all. 9.1.1 lifts that — it no
longer distributes overN, 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.toSortedandArr.toSortedBydeliberately 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, chunkSize)/chunk<N, E><E, N>Arr.toUpdated's curried overload reorders its type parameters to<I, E, V>
for the same reason.Deliberately left alone:
Result.foldandTernaryResult.folddeclare
<S, E, S2, E2>/<S, W, E, S2, W2, E2>, grouping input types before output
types rather than following first use — that mirrors
Result<S, E>→Result<S2, E2>and reads better than the alternative.
TernaryResult<S, E, W = E>keepsWlast so its default can refer toE.BREAKING CHANGE:
Arr.sumByandArr.partition(and itsArr.chunkalias)
swap their two type parameters, and the curriedArr.toUpdatedreorders its to
<I, E, V>. Only call sites passing explicit type arguments are affected;
inference is unchanged.Arr.isArraydetectsunknownandanywithIsUnknown/IsAnyinstead
of re-spelling them asTypeEq<T, unknown>/TypeEq<T, any>, which also drops
theno-explicit-anysuppression the old form needed. Behavior is unchanged.The length guards and casts are regrouped, and the structural tuple family is
completed.isEmpty/isNonEmptynarrow to the branded types, so they now
live with the rest of the branded guards rather than in general validation, and
the structural*Tupleguards move to a file of their own:module holds array-utils-length-bounded-array-guardisMin/Max/Bounded/FixedLengthArray,isEmpty,isNonEmptyarray-utils-length-bounded-array-castasMin/Max/Bounded/FixedLengthArray,asNonEmptyArray,asEmptyArray(new)array-utils-length-bounded-tuple-guardisMin/Max/Bounded/FixedLengthTuple,isEmptyTuple(new),isNonEmptyTuple(new)array-utils-length-bounded-tuple-castasMin/Max/Bounded/FixedLengthTuple,asEmptyTuple,asNonEmptyTuple— all newarray-utils-validationisArray,every,some,indexIsInRangeNew API:
-
Arr.asEmptyArray(xs)— the cast counterpart ofArr.isEmpty, and the
length-0 specialization ofArr.asFixedLengthArray. -
Arr.isEmptyTuple(xs)/Arr.isNonEmptyTuple(xs)— the structural
counterparts ofisEmpty/isNonEmpty, narrowing toreadonly []and
MinLengthTuple<1, E>. -
Arr.asFixedLengthTuple/asMinLengthTuple/asMaxLengthTuple/
asBoundedLengthTuple/asEmptyTuple/asNonEmptyTuple— the cast
counterparts of the structural tuple guards, with the same length-first
argument order and curried overloads as the branded family.These take
Xs extends readonly unknown[]and answer
MinLengthTuple<N, Xs[number]> & Xs, exactly as the branded casts answer
MinLengthArray<N, Xs[number]> & Xs. Typing the parameter asreadonly E[]
instead would widen the caller's type — an exact five-tuple would come back
as "at least three" — which is the one thing a cast should never do, and
which neitherArr.as*ArraynorStr.as*does.
Prefer the branded
*Arrayfamily; the structural*Tupleone is there for
when a tuple type is specifically what you need. Only the module layout moved —
every symbol is still re-exported throughArr, so no import path changes for
consumers.ts-type-forgemoves to the 9.x line. The range goes from~7.2.1to
^9.1.1— a caret rather than a tilde, so the consumer's tree can dedupe it
against their own copy instead of being held to one patch line.It stays in
dependencies, where it already was, and not inpeerDependencies:
ts-type-forge is types-only — itsexportsmap has no runtime entry at all — so
it adds nothing to a bundle, and nothing breaks if two copies end up in the tree.
Its brands are keyed by a string literal rather than aunique symbol, so
separate copies stay structurally compatible. A peer range would push a package
the consumer never named into their install, for no benefit;typescriptstays a
peer dependency because that one genuinely must be shared.BREAKING CHANGE: the
ts-type-forgedependency moves from~7.2.1to^9.1.1,
forChangeArrayElement, the bound accessors it is built on, and the
ConstrainedListnamespace. ts-type-forge types appear in ts-data-forge's own
public signatures, so a project that also depends onts-type-forgedirectly
needs to be on the 9.x line. Three of its breaks are worth naming: 8.0.0
constrains the bounds ofUintRange/UintRangeInclusivetoUint11(ranges
that already fit are unaffected), and 9.0.0 moves the length / index parameter
first onMakeTupleand onList.SetAt/Tuple.SetAt, matching the rest of
that library.New rule
prefer-canonical-length-castineslint-plugin-ts-data-forge,
theArr.as*counterpart ofprefer-canonical-length-guard:❌ written as ✅ canonical form relation Arr.asMinLengthArray(1, xs)Arr.asNonEmptyArray(xs)type-identical Arr.asBoundedLengthArray(n, n, xs)Arr.asFixedLengthArray(n, xs)strengthens Arr.asMaxLengthArray(0, xs)Arr.asEmptyArray(xs)strengthens A cast only ever returns the narrowed value, so strengthening the result type is
safe — it stays assignable everywhere the old one was.
Arr.asBoundedLengthArray(0, n, xs)→Arr.asMaxLengthArray(n, xs)is
deliberately not reported: it would drop theMinLengthArray<0, E>brand, a
widening rather than a rename.Both
prefer-canonical-length-*rules now rewrite within a family: a branded
*Arrayguard or cast normalizes toisEmpty/isNonEmpty/asEmptyArray/
asNonEmptyArray, and a structural*Tupleone to the newisEmptyTuple/
isNonEmptyTuple/asEmptyTuple/asNonEmptyTuple:❌ written as ✅ canonical form Arr.isFixedLengthArray(0, xs)Arr.isEmpty(xs)Arr.isFixedLengthTuple(0, xs)Arr.isEmptyTuple(xs)Arr.isMinLengthTuple(1, xs)Arr.isNonEmptyTuple(xs)Arr.asFixedLengthArray(0, xs)Arr.asEmptyArray(xs)Arr.asFixedLengthTuple(0, xs)Arr.asEmptyTuple(xs)Arr.asBoundedLengthTuple(n, n, xs)Arr.asFixedLengthTuple(n, xs)Before the
*Tupledegenerate guards existed, the only named target was the
brandedisEmpty/isNonEmpty, so rewriting a*Tupleguard strengthened
the narrowed type by adding the brand. Sound, but it silently moved the value
into the other family. No rewrite crosses the branded/structural divide any more;
the few that still strengthen do so within one family, adding the structural part
its named target already carries (e.g.Arr.isMaxLengthArray(0, xs)→
Arr.isEmpty(xs)gains thereadonly []thatFixedLengthArray<0, E>includes
andMaxLengthArray<0, E>does not).BREAKING CHANGE:
prefer-canonical-length-guardpreviously rewrote
Arr.isFixedLengthTuple(0, xs),Arr.isMaxLengthTuple(0, xs),
Arr.isBoundedLengthTuple(0, 0, xs)andArr.isMinLengthTuple(1, xs)to
Arr.isEmpty/Arr.isNonEmpty; it now rewrites them toArr.isEmptyTuple/
Arr.isNonEmptyTuple. Code already autofixed by the old rule keeps compiling —
the branded type it produced is narrower — but re-running the fixer no longer
adds the brand, so a value that relied on it needs the branded guard spelled out.
The newprefer-canonical-length-castfollows the same lanes from the start, so
Arr.asMaxLengthArray(0, xs)goes toArr.asEmptyArray(xs)rather than to
Arr.asFixedLengthArray(0, xs).The new rule is part of the
recommendedconfig preset, so it is enabled for
anyone extendingeslintPluginTsDataForge.configs.recommended.
prefer-canonical-length-guardand thexs.length <op> nfixers it absorbed now
emit the new length-first argument order.The plugin's own
ts-type-forgerange moves withts-data-forge's. It asked
for~7.2.1, the same rangets-data-forgehad; both go to^9.1.1together,
so installing the plugin alongside the library still resolves a single copy.
Nothing about the plugin's behavior changes: it uses ts-type-forge only for
DeepReadonly, applied to@typescript-eslintAST nodes, and v9's change to
that type affects arrays carrying extra keys — the length-constraint brands —
which AST nodes do not have. Its 275 rule tests and the build pass unchanged. The
placement was already right (dependencies, sincedist/types.d.mtsand
dist/rules/ast-utils.d.mtsimport from it).Cost across this package's own suite: +20.1k instantiations, about 1.0%
(1,946,577 against a 1,926,509 baseline). -