From f6530b70f6338d979dca1606ab544f8f44d72b62 Mon Sep 17 00:00:00 2001 From: Connor Shea <2977353+connorshea@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:03:59 -0600 Subject: [PATCH 1/2] Replace ts-toolbelt with local type utilities ramda used twelve types from ts-toolbelt, but its entry point is a namespace barrel that drags all 241 of its files into the program of every project that consumes these types. Deep-importing helped; not depending on it at all helps more, and the twelve are not much code. types/util/_internal.d.ts now implements them. A util file whose name starts with `_` is internal: the build imports from it and ships it, but does not re-export it, so the public API is untouched. Two of the twelve turned out to be TypeScript globals already (`F.Parameters` is `Parameters`, `F.Return` is `ReturnType`), and `T.Merge` was only ever used to swap the first two parameters in `flip`, which `Swap2` now says directly. ts-toolbelt predates TypeScript 4.5, so it counts with a 202-entry lookup table of number literals (`Iteration`) that also caps it at +/-100 elements. The replacements recurse over variadic tuples, so there is no arithmetic, no table, and no upper bound. That is where the drop in instantiations comes from - same results, less work. Measured on a program that does `import * as R from 'types-ramda'` and calls 19 of the exported functions, TypeScript 5.2: before deep imports no ts-toolbelt Files 253 66 13 Memory 92 MB 81 MB 73 MB Instantiations 7,309 7,309 3,441 Verified by a differential harness that asserts each replacement is the *same type* as the ts-toolbelt original - invariant identity, not mutual assignability - across 60 cases covering optional and rest parameters, out-of-range indices, deep merges, arrays, tuples, Date and functions. `Narrow` additionally gets inference probes, because identity alone did not catch that writing its conditional inline instead of routing it through `Try` silently defers it, stops it being an inference site, and widens `['a', 'b']` to `string[]`. `Curry` is checked behaviourally across 24 applications, placeholders included, since two distinct recursive aliases can never compare equal by identity. The generated es/index.d.ts differs from the previous build only in its import header and the renamed references; every declaration is unchanged. tsd passes clean, and ts4/es/index.d.ts still compiles clean under TypeScript 4.9. The package now has no runtime dependencies. Co-Authored-By: Claude Opus 5 (1M context) --- .eslintrc.json | 4 + CONTRIBUTING.md | 5 + package-lock.json | 13 -- package.json | 3 - scripts/buildScripts.mjs | 7 +- types/addIndex.d.ts | 10 +- types/assocPath.d.ts | 4 +- types/binary.d.ts | 5 +- types/construct.d.ts | 4 +- types/constructN.d.ts | 4 +- types/converge.d.ts | 6 +- types/curry.d.ts | 4 +- types/curryN.d.ts | 7 +- types/flatten.d.ts | 4 +- types/flip.d.ts | 6 +- types/mergeAll.d.ts | 4 +- types/mergeDeepLeft.d.ts | 6 +- types/mergeDeepRight.d.ts | 6 +- types/mergeLeft.d.ts | 6 +- types/mergeRight.d.ts | 6 +- types/nAry.d.ts | 7 +- types/pathOr.d.ts | 4 +- types/pathSatisfies.d.ts | 4 +- types/propSatisfies.d.ts | 4 +- types/reduceBy.d.ts | 6 +- types/reduceWhile.d.ts | 6 +- types/symmetricDifferenceWith.d.ts | 4 +- types/thunkify.d.ts | 4 +- types/tryCatch.d.ts | 5 +- types/unionWith.d.ts | 4 +- types/unnest.d.ts | 4 +- types/util/_internal.d.ts | 300 +++++++++++++++++++++++++++++ types/util/tools.d.ts | 12 +- types/util/zipObj.d.ts | 4 +- types/zipObj.d.ts | 8 +- 35 files changed, 391 insertions(+), 99 deletions(-) create mode 100644 types/util/_internal.d.ts diff --git a/.eslintrc.json b/.eslintrc.json index 06ca787a..500c7542 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -160,6 +160,10 @@ "types/**/*.ts", "test/**/*.ts" ], + "globals": { + // a TypeScript lib type with no runtime binding, so `no-undef` cannot see it + "Generator": "readonly" + }, "parser": "@typescript-eslint/parser", "plugins": [ "@typescript-eslint" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1b0337dd..590ff44f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,11 @@ 1. Run `npm install` to install needed local dependencies. +1. This package has no runtime dependencies, and shouldn't gain one. The type-level utilities + it needs (`Curry`, `Assign`, `Narrow`, ...) live in `types/util/_internal.d.ts`. A util file + whose name starts with `_` is internal: `npm run build` imports from it and ships it, but + does not re-export it, so adding to it does not widen the public API. + 1. Run `npm run build` first, then `npm run test` and `npm run lint` and address any errors. Preferably, fix commits in place using `git rebase` or `git commit --amend` to make the changes easier to review and to keep the history tidy. diff --git a/package-lock.json b/package-lock.json index ac90069f..2ee240d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "types-ramda", "version": "0.32.0", "license": "MIT", - "dependencies": { - "ts-toolbelt": "^9.6.0" - }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", @@ -3672,11 +3669,6 @@ "typescript": ">=4.2.0" } }, - "node_modules/ts-toolbelt": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", - "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==" - }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -6660,11 +6652,6 @@ "dev": true, "requires": {} }, - "ts-toolbelt": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", - "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==" - }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", diff --git a/package.json b/package.json index c2ed2132..db7d81ed 100644 --- a/package.json +++ b/package.json @@ -58,9 +58,6 @@ "tsd": { "directory": "test/" }, - "dependencies": { - "ts-toolbelt": "^9.6.0" - }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", diff --git a/scripts/buildScripts.mjs b/scripts/buildScripts.mjs index 37d1a0f6..6a396808 100644 --- a/scripts/buildScripts.mjs +++ b/scripts/buildScripts.mjs @@ -143,6 +143,10 @@ const genExport = (x) => { const genExports = (types) => types.map(genExport).join('\n\n'); +// A util file whose name starts with `_` is internal: index.d.ts imports from it and it ships +// alongside the other declarations, but it is not re-exported as part of the public API. +const isInternal = (utilPath) => basename(utilPath).startsWith('_'); + export const write = (utilDir, outDir, types) => { const utilPaths = readdirSync(utilDir).map(p => join(utilDir, p)); @@ -151,12 +155,11 @@ export const write = (utilDir, outDir, types) => { const exportsCode = genExports(types); const otherExports = [ - ...utilPaths.map(p => `export * from './${basename(p, '.d.ts')}';`), + ...utilPaths.filter(p => !isInternal(p)).map(p => `export * from './${basename(p, '.d.ts')}';`), 'export as namespace R;' ].join('\n'); const code = [ - 'import * as _ from \'ts-toolbelt\';', ...importsCode, '', exportsCode, diff --git a/types/addIndex.d.ts b/types/addIndex.d.ts index 57be3d92..19bdaff0 100644 --- a/types/addIndex.d.ts +++ b/types/addIndex.d.ts @@ -1,18 +1,18 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; // Special case for forEach export function addIndex( fn: (f: (item: T) => void, list: readonly T[]) => T[], -): _.F.Curry<(a: (item: T, idx: number, list: T[]) => void, b: readonly T[]) => T[]>; +): Curry<(a: (item: T, idx: number, list: T[]) => void, b: readonly T[]) => T[]>; // Special case for filter export function addIndex( fn: (f: (item: T) => boolean, list: readonly T[]) => T[], -): _.F.Curry<(a: (item: T, idx: number, list: T[]) => boolean, b: readonly T[]) => T[]>; +): Curry<(a: (item: T, idx: number, list: T[]) => boolean, b: readonly T[]) => T[]>; // Special case for map export function addIndex( fn: (f: (item: T) => U, list: readonly T[]) => U[], -): _.F.Curry<(a: (item: T, idx: number, list: T[]) => U, b: readonly T[]) => U[]>; +): Curry<(a: (item: T, idx: number, list: T[]) => U, b: readonly T[]) => U[]>; // Special case for reduce export function addIndex( fn: (f: (acc: U, item: T) => U, aci: U, list: readonly T[]) => U, -): _.F.Curry<(a: (acc: U, item: T, idx: number, list: T[]) => U, b: U, c: readonly T[]) => U>; +): Curry<(a: (acc: U, item: T, idx: number, list: T[]) => U, b: U, c: readonly T[]) => U>; diff --git a/types/assocPath.d.ts b/types/assocPath.d.ts index b53afc81..9d0da053 100644 --- a/types/assocPath.d.ts +++ b/types/assocPath.d.ts @@ -1,7 +1,7 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; import { Placeholder, Path } from './util/tools'; -export function assocPath(path: Path): _.F.Curry<(a: T, b: U) => U>; +export function assocPath(path: Path): Curry<(a: T, b: U) => U>; export function assocPath(path: Path, val: T): (obj: U) => U; export function assocPath(__: Placeholder, val: T, obj: U): (path: Path) => U; export function assocPath(path: Path, __: Placeholder, obj: U): (val: T) => U; diff --git a/types/binary.d.ts b/types/binary.d.ts index f8067900..e8e56475 100644 --- a/types/binary.d.ts +++ b/types/binary.d.ts @@ -1,4 +1,3 @@ -import * as _ from 'ts-toolbelt'; -import { Take } from './util/tools'; +import { TakeFirst } from './util/_internal'; -export function binary any>(fn: T): (...arg: _.T.Take, 2>) => ReturnType; +export function binary any>(fn: T): (...arg: TakeFirst, 2>) => ReturnType; diff --git a/types/construct.d.ts b/types/construct.d.ts index 450915ab..9995be9e 100644 --- a/types/construct.d.ts +++ b/types/construct.d.ts @@ -1,6 +1,6 @@ +import { Curry } from './util/_internal'; -import * as _ from 'ts-toolbelt'; export function construct( constructor: { new (...a: A): T } | ((...a: A) => T), -): _.F.Curry<(...a: A) => T>; +): Curry<(...a: A) => T>; diff --git a/types/constructN.d.ts b/types/constructN.d.ts index 07e42dfa..b4ec6f28 100644 --- a/types/constructN.d.ts +++ b/types/constructN.d.ts @@ -1,8 +1,8 @@ +import { Curry } from './util/_internal'; -import * as _ from 'ts-toolbelt'; import { mergeArrWithLeft, Tuple } from './util/tools'; export function constructN( n: N, constructor: { new (...a: A): T } | ((...a: A) => T), -): _.F.Curry<(...a: mergeArrWithLeft, A>) => T>; +): Curry<(...a: mergeArrWithLeft, A>) => T>; diff --git a/types/converge.d.ts b/types/converge.d.ts index d9b912b6..11af82a7 100644 --- a/types/converge.d.ts +++ b/types/converge.d.ts @@ -1,4 +1,4 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; import { Fn, IfFunctionsArgumentsDoNotOverlap, ReturnTypesOfFns, LargestArgumentsList } from './util/tools'; export function converge< @@ -9,7 +9,7 @@ export function converge< >( converging: (...args: ReturnTypesOfFns) => TResult, branches: FunctionsList, -): _.F.Curry<(...args: LargestArgumentsList) => TResult>; +): Curry<(...args: LargestArgumentsList) => TResult>; export function converge< CArgs extends ReadonlyArray, TResult, @@ -23,4 +23,4 @@ export function converge< >( converging: (...args: CArgs) => TResult, branches: FunctionsList, -): _.F.Curry<(...args: LargestArgumentsList) => TResult>; +): Curry<(...args: LargestArgumentsList) => TResult>; diff --git a/types/curry.d.ts b/types/curry.d.ts index 4ff8c8f0..41f5e43c 100644 --- a/types/curry.d.ts +++ b/types/curry.d.ts @@ -1,3 +1,3 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; -export function curry any>(f: F): _.F.Curry; +export function curry any>(f: F): Curry; diff --git a/types/curryN.d.ts b/types/curryN.d.ts index fb019ee7..db145f37 100644 --- a/types/curryN.d.ts +++ b/types/curryN.d.ts @@ -1,12 +1,11 @@ -import * as _ from 'ts-toolbelt'; -import { Take } from './util/tools'; +import { Curry, TakeFirst } from './util/_internal'; export function curryN( length: N, ): any>( fn: F, -) => _.F.Curry<(...a: _.T.Take, N>) => ReturnType>; +) => Curry<(...a: TakeFirst, N>) => ReturnType>; export function curryN any>( length: N, fn: F, -): _.F.Curry<(...a: _.T.Take, N>) => ReturnType>; +): Curry<(...a: TakeFirst, N>) => ReturnType>; diff --git a/types/flatten.d.ts b/types/flatten.d.ts index 980098f0..cef288d1 100644 --- a/types/flatten.d.ts +++ b/types/flatten.d.ts @@ -1,3 +1,3 @@ -import * as _ from 'ts-toolbelt'; +import { Flatten } from './util/_internal'; -export function flatten(list: T): _.T.Flatten; +export function flatten(list: T): Flatten; diff --git a/types/flip.d.ts b/types/flip.d.ts index d72025e6..7c5f96e9 100644 --- a/types/flip.d.ts +++ b/types/flip.d.ts @@ -1,9 +1,9 @@ -import * as _ from 'ts-toolbelt'; +import { Curry, Swap2 } from './util/_internal'; export function flip(fn: (arg0: T, arg1: U) => TResult): { (arg1: U): (arg0: T) => TResult; (arg1: U, arg0: T): TResult; }; -export function flip any, P extends _.F.Parameters>( +export function flip any, P extends Parameters>( fn: F, -): _.F.Curry<(...args: _.T.Merge<[P[1], P[0]], P>) => _.F.Return>; +): Curry<(...args: Swap2

) => ReturnType>; diff --git a/types/mergeAll.d.ts b/types/mergeAll.d.ts index c0e7cde2..67d19283 100644 --- a/types/mergeAll.d.ts +++ b/types/mergeAll.d.ts @@ -1,8 +1,8 @@ -import * as _ from 'ts-toolbelt'; +import { Assign } from './util/_internal'; // for when passing in an object literal of different objects, eg `mergeAll([obj1: T1, obj2: T2, obj3: T3])` // you get back essentially a cleaner version of `T1 & T2 & T3` -export function mergeAll(list: [T, ...Ts]): _.O.Assign; +export function mergeAll(list: [T, ...Ts]): Assign; // for when passing in an `T[]` where all the objects are the same shape `mergeAll([obj1, obj2, obj3]: T[]) // this just returns T export function mergeAll(list: readonly T[]): T; diff --git a/types/mergeDeepLeft.d.ts b/types/mergeDeepLeft.d.ts index 31621088..e880b638 100644 --- a/types/mergeDeepLeft.d.ts +++ b/types/mergeDeepLeft.d.ts @@ -1,4 +1,4 @@ -import * as _ from 'ts-toolbelt'; +import { Assign } from './util/_internal'; -export function mergeDeepLeft(l: L): (r: R) => _.O.Assign; -export function mergeDeepLeft(l: L, r: R): _.O.Assign; +export function mergeDeepLeft(l: L): (r: R) => Assign; +export function mergeDeepLeft(l: L, r: R): Assign; diff --git a/types/mergeDeepRight.d.ts b/types/mergeDeepRight.d.ts index 8824416b..9aff4cd4 100644 --- a/types/mergeDeepRight.d.ts +++ b/types/mergeDeepRight.d.ts @@ -1,4 +1,4 @@ -import * as _ from 'ts-toolbelt'; +import { Assign } from './util/_internal'; -export function mergeDeepRight(l: L): (r: R) => _.O.Assign; -export function mergeDeepRight(l: L, r: R): _.O.Assign; +export function mergeDeepRight(l: L): (r: R) => Assign; +export function mergeDeepRight(l: L, r: R): Assign; diff --git a/types/mergeLeft.d.ts b/types/mergeLeft.d.ts index a9b2b1f7..1570345a 100644 --- a/types/mergeLeft.d.ts +++ b/types/mergeLeft.d.ts @@ -1,5 +1,5 @@ -import * as _ from 'ts-toolbelt'; +import { Assign } from './util/_internal'; // Note: ramda `mergeLeft` uses `Object.assign` in code, so we need to use `O.Assign` here, and not `O.Merge` -export function mergeLeft(l: L): (r: R) => _.O.Assign; -export function mergeLeft(l: L, r: R): _.O.Assign; +export function mergeLeft(l: L): (r: R) => Assign; +export function mergeLeft(l: L, r: R): Assign; diff --git a/types/mergeRight.d.ts b/types/mergeRight.d.ts index 075e08a5..283438f3 100644 --- a/types/mergeRight.d.ts +++ b/types/mergeRight.d.ts @@ -1,5 +1,5 @@ -import * as _ from 'ts-toolbelt'; +import { Assign } from './util/_internal'; // Note: ramda `mergeLeft` uses `Object.assign` in code, so we need to use `O.Assign` here, and not `O.Merge` -export function mergeRight(l: L): (r: R) => _.O.Assign; -export function mergeRight(l: L, r: R): _.O.Assign; +export function mergeRight(l: L): (r: R) => Assign; +export function mergeRight(l: L, r: R): Assign; diff --git a/types/nAry.d.ts b/types/nAry.d.ts index 90561381..62f1323e 100644 --- a/types/nAry.d.ts +++ b/types/nAry.d.ts @@ -1,10 +1,9 @@ -import * as _ from 'ts-toolbelt'; -import { Take } from './util/tools'; +import { TakeFirst } from './util/_internal'; export function nAry( n: N, -): unknown>(fn: T) => (...arg: _.T.Take, N>) => ReturnType; +): unknown>(fn: T) => (...arg: TakeFirst, N>) => ReturnType; export function nAry unknown>( n: N, fn: T, -): (...arg: _.T.Take, N>) => ReturnType; +): (...arg: TakeFirst, N>) => ReturnType; diff --git a/types/pathOr.d.ts b/types/pathOr.d.ts index a1468d7e..9dcdc637 100644 --- a/types/pathOr.d.ts +++ b/types/pathOr.d.ts @@ -1,6 +1,6 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; import { Path } from './util/tools'; -export function pathOr(defaultValue: T): _.F.Curry<(a: Path, b: any) => T>; +export function pathOr(defaultValue: T): Curry<(a: Path, b: any) => T>; export function pathOr(defaultValue: T, path: Path): (obj: any) => T; export function pathOr(defaultValue: T, path: Path, obj: any): T; diff --git a/types/pathSatisfies.d.ts b/types/pathSatisfies.d.ts index 12efb29c..8c8796b7 100644 --- a/types/pathSatisfies.d.ts +++ b/types/pathSatisfies.d.ts @@ -1,6 +1,6 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; import { Path } from './util/tools'; -export function pathSatisfies(pred: (val: T) => boolean): _.F.Curry<(a: Path, b: U) => boolean>; +export function pathSatisfies(pred: (val: T) => boolean): Curry<(a: Path, b: U) => boolean>; export function pathSatisfies(pred: (val: T) => boolean, path: Path): (obj: U) => boolean; export function pathSatisfies(pred: (val: T) => boolean, path: Path, obj: U): boolean; diff --git a/types/propSatisfies.d.ts b/types/propSatisfies.d.ts index e20e0414..36f84a84 100644 --- a/types/propSatisfies.d.ts +++ b/types/propSatisfies.d.ts @@ -1,4 +1,4 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; export function propSatisfies( pred: (val: any) => val is P, @@ -15,4 +15,4 @@ export function propSatisfies

(pred: (val: any) => val is P): { }; export function propSatisfies(pred: (val: any) => boolean, name: keyof any, obj: any): boolean; export function propSatisfies(pred: (val: any) => boolean, name: keyof any): (obj: any) => boolean; -export function propSatisfies(pred: (val: any) => boolean): _.F.Curry<(a: keyof any, b: any) => boolean>; +export function propSatisfies(pred: (val: any) => boolean): Curry<(a: keyof any, b: any) => boolean>; diff --git a/types/reduceBy.d.ts b/types/reduceBy.d.ts index 8c64b5a2..6e5e49f4 100644 --- a/types/reduceBy.d.ts +++ b/types/reduceBy.d.ts @@ -1,12 +1,12 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; export function reduceBy( valueFn: (acc: TResult, elem: T) => TResult, -): _.F.Curry<(a: TResult, b: (elem: T) => string, c: readonly T[]) => { [index: string]: TResult }>; +): Curry<(a: TResult, b: (elem: T) => string, c: readonly T[]) => { [index: string]: TResult }>; export function reduceBy( valueFn: (acc: TResult, elem: T) => TResult, acc: TResult, -): _.F.Curry<(a: (elem: T) => string, b: readonly T[]) => { [index: string]: TResult }>; +): Curry<(a: (elem: T) => string, b: readonly T[]) => { [index: string]: TResult }>; export function reduceBy( valueFn: (acc: TResult, elem: T) => TResult, acc: TResult, diff --git a/types/reduceWhile.d.ts b/types/reduceWhile.d.ts index b164b45e..52e842d1 100644 --- a/types/reduceWhile.d.ts +++ b/types/reduceWhile.d.ts @@ -1,12 +1,12 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; export function reduceWhile( predicate: (acc: TResult, elem: T) => boolean, -): _.F.Curry<(a: (acc: TResult, elem: T) => TResult, b: TResult, c: readonly T[]) => TResult>; +): Curry<(a: (acc: TResult, elem: T) => TResult, b: TResult, c: readonly T[]) => TResult>; export function reduceWhile( predicate: (acc: TResult, elem: T) => boolean, fn: (acc: TResult, elem: T) => TResult, -): _.F.Curry<(a: TResult, b: readonly T[]) => TResult>; +): Curry<(a: TResult, b: readonly T[]) => TResult>; export function reduceWhile( predicate: (acc: TResult, elem: T) => boolean, fn: (acc: TResult, elem: T) => TResult, diff --git a/types/symmetricDifferenceWith.d.ts b/types/symmetricDifferenceWith.d.ts index d2771ef2..d160ddae 100644 --- a/types/symmetricDifferenceWith.d.ts +++ b/types/symmetricDifferenceWith.d.ts @@ -1,8 +1,8 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; export function symmetricDifferenceWith( pred: (a: T, b: T) => boolean, -): _.F.Curry<(a: readonly T[], b: readonly T[]) => T[]>; +): Curry<(a: readonly T[], b: readonly T[]) => T[]>; export function symmetricDifferenceWith( pred: (a: T, b: T) => boolean, diff --git a/types/thunkify.d.ts b/types/thunkify.d.ts index 810ef119..1af935de 100644 --- a/types/thunkify.d.ts +++ b/types/thunkify.d.ts @@ -1,5 +1,5 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; export function thunkify any>( fn: F, -): _.F.Curry<(...args: Parameters) => () => ReturnType>; +): Curry<(...args: Parameters) => () => ReturnType>; diff --git a/types/tryCatch.d.ts b/types/tryCatch.d.ts index 1c4b349b..77d7e91a 100644 --- a/types/tryCatch.d.ts +++ b/types/tryCatch.d.ts @@ -1,9 +1,8 @@ -import * as _ from 'ts-toolbelt'; export function tryCatch any>( tryer: F, -): , E = unknown>(catcher: (error: E, ...args: _.F.Parameters) => RE) => F | (() => RE); +): , E = unknown>(catcher: (error: E, ...args: Parameters) => RE) => F | (() => RE); export function tryCatch any, RE = ReturnType, E = unknown>( tryer: F, - catcher: (error: E, ...args: _.F.Parameters) => RE, + catcher: (error: E, ...args: Parameters) => RE, ): F | (() => RE); diff --git a/types/unionWith.d.ts b/types/unionWith.d.ts index 2d4b1c06..93f7901d 100644 --- a/types/unionWith.d.ts +++ b/types/unionWith.d.ts @@ -1,4 +1,4 @@ -import * as _ from 'ts-toolbelt'; +import { Curry } from './util/_internal'; -export function unionWith(pred: (a: T, b: T) => boolean): _.F.Curry<(a: readonly T[], b: readonly T[]) => T[]>; +export function unionWith(pred: (a: T, b: T) => boolean): Curry<(a: readonly T[], b: readonly T[]) => T[]>; export function unionWith(pred: (a: T, b: T) => boolean, list1: readonly T[], list2: readonly T[]): T[]; diff --git a/types/unnest.d.ts b/types/unnest.d.ts index 9adfb4cd..5033c3ce 100644 --- a/types/unnest.d.ts +++ b/types/unnest.d.ts @@ -1,3 +1,3 @@ -import * as _ from 'ts-toolbelt'; +import { UnNest } from './util/_internal'; -export function unnest(list: T): _.T.UnNest; +export function unnest(list: T): UnNest; diff --git a/types/util/_internal.d.ts b/types/util/_internal.d.ts new file mode 100644 index 00000000..16114d0d --- /dev/null +++ b/types/util/_internal.d.ts @@ -0,0 +1,300 @@ +// Type-level utilities that used to come from `ts-toolbelt`. +// +// ramda needed twelve types out of that package, but its entry point is a namespace barrel that +// drags all 241 of its files into the program of every project consuming these types. The +// implementations below replace them. They are deliberately NOT re-exported from the package's +// public surface - `_`-prefixed files under `types/util` are internal to the build. +// +// ts-toolbelt predates TypeScript 4.5, so it counted with a 202-entry lookup table of number +// literals (`Iteration`), which is what made it expensive to check and capped it at +/-100 +// elements. Everything here recurses over variadic tuples instead, so there is no arithmetic and +// no upper bound. + +type AnyList = readonly unknown[]; + +/** `A1 extends A2 ? A1 : A2` - reassures the checker that a computed type fits a constraint. */ +type Cast = A1 extends A2 ? A1 : A2; + +/** + * The placeholder's brand. `R.__` is the only value of this type, and `Curry` treats an argument + * of this type as "skip this parameter, I'll supply it later". + */ +declare const placeholder: unique symbol; +export type PlaceholderBrand = typeof placeholder & {}; + +/** + * All the primitive types. + */ +export type Primitive = boolean | string | number | bigint | symbol | undefined | null; + +/** + * Take the first `N` elements of list `L`, positionally: the result is always exactly `N` long, + * padding with `undefined` past the end of `L` and repeating `L`'s rest element where it has one. + * A non-literal `N` yields `[]`. + */ +export type TakeFirst = + number extends N + ? [] + : Acc['length'] extends N + ? Acc + : L extends readonly [infer H, ...infer T] + ? TakeFirst + : number extends L['length'] + ? TakeFirst + : L extends readonly [] + ? TakeFirst + : L extends readonly [(infer H)?, ...infer T] + ? TakeFirst + : TakeFirst; + +/** + * Swap the first two elements of a list, leaving the rest in place. Used by `flip`. + */ +export type Swap2 = L extends readonly [infer A, infer B, ...infer R] + ? [B, A, ...R] + : L; + +/** + * Flatten list `L` by one level. + */ +export type UnNest = number extends L['length'] + ? (L[number] extends infer E ? (E extends AnyList ? E[number] : E) : never)[] + : _UnNest; + +type _UnNest = L extends readonly [infer H, ...infer T] + ? [...(H extends AnyList ? H : [H]), ..._UnNest] + : []; + +/** + * Flatten list `L` all the way down. + */ +export type Flatten = number extends L['length'] + ? UnNest + : _Flatten; + +type _Flatten = L extends readonly [infer H, ...infer T] + ? [...(H extends AnyList ? _Flatten : [H]), ..._Flatten] + : []; + +/** + * Build an object from a list of keys and a list of values, pairing them by position. Keys past + * the end of the values get `undefined`, surplus values are dropped, and where a key repeats the + * first occurrence wins. + */ +export type ZipObj = _ZipObjAcc extends infer O + ? { [P in keyof O]: O[P] } + : never; + +type _ZipObjAcc = K extends readonly [infer KH, ...infer KT] + ? _ZipObjAcc< + KT, + V extends readonly [unknown, ...infer VT] ? VT : [], + KH extends PropertyKey + ? KH extends keyof Acc + ? Acc + : Acc & Record + : Acc + > + : Acc; + +// --------------------------------------------------------------------------------------------- +// Curry +// --------------------------------------------------------------------------------------------- + +type RemoveNullish = A extends null | undefined ? never : A; + +/** + * Every parameter becomes optional and may be filled with the placeholder instead. + */ +type Gaps = Cast< +{ [K in keyof L]?: L[K] | PlaceholderBrand } extends infer G ? { [K in keyof G]: RemoveNullish } & {} : never, +AnyList +>; + +/** + * The parameters still outstanding after `P` has been applied to a function taking `L`: a + * placeholder in `P` keeps the parameter under it, anything else consumes it, and parameters + * past the end of `P` are all still outstanding. + */ +type GapsOf

= P extends readonly [infer PH, ...infer PT] + ? L extends readonly [infer LH, ...infer LT] + ? PH extends PlaceholderBrand + ? [LH, ...GapsOf] + : GapsOf + : [] + : L; + +/** Whether every remaining parameter is optional, ie. the call can be made now. */ +type NoneRequired = L extends readonly [unknown, ...unknown[]] ? false : true; + +/** + * A curried function: it accepts any prefix of its parameters, with `R.__` standing in for any + * parameter to be supplied later, and returns either the result or a curried function taking + * whatever is left. + */ +export type Curry any> = < + P extends Gaps>, + G extends AnyList = GapsOf>, + R = ReturnType +>( + ...p: Gaps> | P +) => NoneRequired extends true ? R : Curry<(...p: G) => R>; + +// --------------------------------------------------------------------------------------------- +// Narrow +// --------------------------------------------------------------------------------------------- + +type Narrowable = string | number | bigint | boolean; + +type NarrowRaw = + | (A extends [] ? [] : never) + | (A extends Narrowable ? A : never) + | { [K in keyof A]: A[K] extends Function ? A[K] : NarrowRaw }; + +/** + * `A1 extends A2 ? A1 : Catch` - like `Cast` but with a chosen fallback. + * + * `Narrow` must route through this rather than writing the conditional inline. Spelling it as + * `A extends [] ? A : NarrowRaw` produces an identical type but defers the conditional, and a + * deferred conditional is not an inference site - the mapped type inside `NarrowRaw` stops being + * visible to inference and arguments widen (`['a', 'b']` infers as `string[]`, not `['a', 'b']`). + * Passing `NarrowRaw` as an argument here keeps it visible. + */ +type Try = A1 extends A2 ? A1 : Catch; + +/** + * Infer a literal type for `A` rather than widening it, the way a `const` assertion would. + */ +export type Narrow = Try>; + +// --------------------------------------------------------------------------------------------- +// Assign +// --------------------------------------------------------------------------------------------- + +/** Types that merging treats as opaque values rather than recursing into. */ +type BuiltIn = Function | Error | Date | { readonly [Symbol.toStringTag]: string } | RegExp | Generator; + +type Anyfy = { [K in keyof O]: any }; + +/** `O[K]`, or `undefined` where `O` has no such key. */ +type At = A extends AnyList + ? number extends A['length'] + ? K extends number | `${number}` + ? A[never] | undefined + : undefined + : K extends keyof A + ? A[K] + : undefined + : unknown extends A + ? unknown + : K extends keyof A + ? A[K] + : undefined; + +type OptionalKeysOf = { [K in keyof O]-?: {} extends Pick ? K : never }[keyof O]; +type RequiredKeysOf = { [K in keyof O]-?: {} extends Pick ? never : K }[keyof O]; + +type ListObjectOf = number extends L['length'] + ? Pick + : Omit; + +/** 1 when `L` has at least as many required elements as `L1`. */ +type Longer = L extends unknown + ? L1 extends unknown + ? [RequiredKeysOf>] extends [RequiredKeysOf>] + ? 1 + : 0 + : never + : never; + +/** + * One property of a merge: the left side wins, except that an optional key on the left unions + * with the right, and a key missing from the left falls through to it. + */ +type MergeProp = K extends OOKeys + ? Exclude | O1K + : [OK] extends [never] + ? O1K + : OK extends undefined + ? O1K + : OK; + +type MergeFlatObject> = { + [K in keyof (Anyfy & O1)]: MergeProp, At, OOKeys, K>; +} & {}; + +type MergeFlatList> = + number extends (L | L1)['length'] + ? MergeFlatChoice[] + : Longer extends 1 + ? { [K in keyof L]: MergeProp, OOKeys, K> } + : { [K in keyof L1]: MergeProp, L1[K], OOKeys, K> }; + +type MergeFlatChoice = O extends BuiltIn + ? O + : O1 extends BuiltIn + ? O + : O extends AnyList + ? O1 extends AnyList + ? MergeFlatList + : MergeFlatObject + : MergeFlatObject; + +type MergeFlat = O extends unknown + ? O1 extends unknown + ? MergeFlatChoice + : never + : never; + +type MergeDeepList = number extends (L | L1)['length'] + ? MergeDeepChoice[] + : Longer extends 1 + ? { [K in keyof L]: MergeDeepChoice, OptionalKeysOf, K> } + : { [K in keyof L1]: MergeDeepChoice, L1[K], OptionalKeysOf, K> }; + +type MergeDeepObject> = { + [K in keyof (Anyfy & O1)]: MergeDeepChoice, At, OOKeys, K>; +}; + +type MergeDeepChoice = [OK] extends [never] + ? MergeProp + : [O1K] extends [never] + ? MergeProp + : OK extends BuiltIn + ? MergeProp + : O1K extends BuiltIn + ? MergeProp + : OK extends AnyList + ? O1K extends AnyList + ? MergeDeepList + : MergeProp + : OK extends object + ? O1K extends object + ? MergeDeepObject + : MergeProp + : MergeProp; + +type MergeDeep = O extends unknown + ? O1 extends unknown + ? MergeDeepChoice + : never + : never; + +type Merge = { + flat: MergeFlat; + deep: MergeDeep; +}[depth]; + +/** + * Fold the objects of `Os` onto `O` from left to right, so later objects win. `depth` chooses + * whether nested objects are replaced wholesale or merged recursively. + */ +export type Assign = + O extends unknown ? (Os extends unknown ? _Assign : never) : never; + +type _Assign = Os extends readonly [ + infer Head, + ...infer Tail +] + ? _Assign, O, depth>, object>, Tail, depth> + : O; diff --git a/types/util/tools.d.ts b/types/util/tools.d.ts index 679e6293..0eb1fe74 100644 --- a/types/util/tools.d.ts +++ b/types/util/tools.d.ts @@ -1,4 +1,4 @@ -import { A, M } from 'ts-toolbelt'; +import { PlaceholderBrand, Primitive } from './_internal'; // Here lies a loose collection of tools that compute types for the functions in "index.d.ts" // The goal of this file is to keep "index.d.ts" readable as well as hiding implementations @@ -168,7 +168,7 @@ type Arr1LessThanOrEqual< export type InferAllAType = T extends { all: (fn: (a: infer A) => boolean) => boolean } ? A : never; /** - * Return true if types T1 and T2 can intersect, e.g. both are primitives or both are objects. + * ReturnType true if types T1 and T2 can intersect, e.g. both are primitives or both are objects. * Taking into account branded types too. * * @param T1 First readonly array @@ -184,8 +184,8 @@ type Intersectable = [T1] extends [T2] ? [T2] extends [object] ? true : false - : [T1] extends [M.Primitive] - ? [T2] extends [M.Primitive] + : [T1] extends [Primitive] + ? [T2] extends [Primitive] ? true : false : false; @@ -372,7 +372,7 @@ export type Path = Array; /** * A placeholder used to skip parameters, instead adding a parameter to the returned function. */ -export type Placeholder = A.x & { '@@functional/placeholder': true }; +export type Placeholder = PlaceholderBrand & { '@@functional/placeholder': true }; /** * A runtime-branded value used to stop `reduce` and `transduce` early. @@ -454,7 +454,7 @@ export type ToTupleOfArray = Tuple extends [] /** * Map tuple of ordinary type to tuple of function type * @param R Parameter type of every function - * @param Tuple Return type of every function + * @param Tuple ReturnType type of every function */ export type ToTupleOfFunction = Tuple extends [] ? [] diff --git a/types/util/zipObj.d.ts b/types/util/zipObj.d.ts index fd0d828a..ec0b88dc 100644 --- a/types/util/zipObj.d.ts +++ b/types/util/zipObj.d.ts @@ -1,7 +1,7 @@ -import * as _ from 'ts-toolbelt'; +import { ZipObj } from './_internal'; export type _ZipObj = number extends V['length'] ? { [T in K[number]]: V[number] } : number extends K['length'] ? - { [T in K[number]]: V[number] } : _.L.ZipObj; + { [T in K[number]]: V[number] } : ZipObj; diff --git a/types/zipObj.d.ts b/types/zipObj.d.ts index 6432e0b9..da2a5ef3 100644 --- a/types/zipObj.d.ts +++ b/types/zipObj.d.ts @@ -1,9 +1,9 @@ -import * as _ from 'ts-toolbelt'; +import { Narrow } from './util/_internal'; import { _ZipObj } from './util/zipObj'; -export function zipObj(keys: _.F.Narrow): -(values: _.F.Narrow) => +export function zipObj(keys: Narrow): +(values: Narrow) => _ZipObj; -export function zipObj(keys: _.F.Narrow, values: _.F.Narrow): +export function zipObj(keys: Narrow, values: Narrow): _ZipObj; From 6e47db61219490379943223e94b82472d6f239ab Mon Sep 17 00:00:00 2001 From: Connor Shea <2977353+connorshea@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:40:37 -0600 Subject: [PATCH 2/2] Fix Flatten, and cover the replaced utilities with tests Flatten had to un-nest to a fixpoint rather than recurse into each element: a tuple can hold a plain array (`[number[][], string]`), and un-nesting that degrades the whole thing to an array which still has levels left to remove. Recursing per element stopped early on `number[][][]` and dropped the array out of the tuple entirely. Nothing caught that, because twelve of the utilities this package took from ts-toolbelt had no tests reaching them. A mutation sweep over `types/util/_internal.d.ts` - break one behaviour, run the suite - left 13 of 17 mutations alive. Add tests for curry, curryN, binary, nAry, flatten, unnest, mergeDeepLeft and mergeDeepRight, and extend the flip, mergeLeft and zipObj tests, which brings the survivors down to two: - `Primitive`'s `bigint`: the only branch it gates produces a `never` intersection either way. - `MergeProp`'s optional-key branch: an optional key's type always includes `undefined`, so the `OK extends undefined` fallback covers it. Both are unobservable through the public API and kept for fidelity with the original. Also drop `TakeFirst`'s `number extends N` guard: `Acc` starts empty and `0 extends number`, so the very next conditional already returns `[]`. Co-Authored-By: Claude Opus 5 (1M context) --- test/binary.test.ts | 18 +++++++++++++++ test/curry.test.ts | 17 ++++++++++++++ test/curryN.test.ts | 15 +++++++++++++ test/flatten.test.ts | 10 +++++++++ test/flip.test.ts | 3 +++ test/mergeDeepLeft.test.ts | 8 +++++++ test/mergeDeepRight.test.ts | 12 ++++++++++ test/mergeLeft.test.ts | 6 +++++ test/nAry.test.ts | 17 ++++++++++++++ test/unnest.test.ts | 7 ++++++ test/zipObj.test.ts | 3 +++ types/util/_internal.d.ts | 44 ++++++++++++++++++++----------------- 12 files changed, 140 insertions(+), 20 deletions(-) create mode 100644 test/binary.test.ts create mode 100644 test/curry.test.ts create mode 100644 test/curryN.test.ts create mode 100644 test/flatten.test.ts create mode 100644 test/mergeDeepLeft.test.ts create mode 100644 test/mergeDeepRight.test.ts create mode 100644 test/nAry.test.ts create mode 100644 test/unnest.test.ts diff --git a/test/binary.test.ts b/test/binary.test.ts new file mode 100644 index 00000000..f5788b25 --- /dev/null +++ b/test/binary.test.ts @@ -0,0 +1,18 @@ +import { expectType } from 'tsd'; + +import { binary } from '../es'; + +const f = (a: number, b: string, c: boolean) => 1; + +expectType(binary(f)(1, 'x')); + +const rest = (...args: string[]) => 1; + +// the rest element supplies both parameters, and supplies them exactly - not as `string | +// undefined`, the way running off the end of a fixed-length list would +expectType<(arg_0: string, arg_1: string) => number>(binary(rest)); + +const one = (a: number) => 1; + +// a function shorter than the arity is padded out, not truncated +expectType(binary(one)(1, undefined)); diff --git a/test/curry.test.ts b/test/curry.test.ts new file mode 100644 index 00000000..eea64ed6 --- /dev/null +++ b/test/curry.test.ts @@ -0,0 +1,17 @@ +import { expectType } from 'tsd'; + +import { curry, __ } from '../es'; + +const f3 = (a: number, b: string, c: boolean) => 'r'; + +// every way of splitting the arguments lands on the same result +expectType(curry(f3)(1, 'a', true)); +expectType(curry(f3)(1)('a')(true)); +expectType(curry(f3)(1, 'a')(true)); +expectType(curry(f3)(1)('a', true)); + +// `__` is accepted in any position, and holds the parameter under it open for a later call +expectType(curry(f3)(__, 'a')(1, true)); +expectType(curry(f3)(__, __, true)(1, 'a')); +expectType(curry(f3)(1, __, true)('a')); +expectType(curry(f3)(__, 'a', true)(1)); diff --git a/test/curryN.test.ts b/test/curryN.test.ts new file mode 100644 index 00000000..636aa6d8 --- /dev/null +++ b/test/curryN.test.ts @@ -0,0 +1,15 @@ +import { expectType } from 'tsd'; + +import { curryN } from '../es'; + +const optional = (a: number, b?: string) => true; + +// an optional parameter is still one of the N parameters being curried +expectType(curryN(2, optional)(1)('x')); +expectType(curryN(2, optional)(1, 'x')); + +const rest = (a: number, ...others: string[]) => true; + +// N reaches into the rest element rather than stopping at the last fixed parameter +expectType(curryN(3, rest)(1)('x')('y')); +expectType(curryN(3, rest)(1, 'x', 'y')); diff --git a/test/flatten.test.ts b/test/flatten.test.ts new file mode 100644 index 00000000..8be39500 --- /dev/null +++ b/test/flatten.test.ts @@ -0,0 +1,10 @@ +import { expectType } from 'tsd'; + +import { flatten } from '../es'; + +// flattens all the way down, not one level +expectType<[1, 2, 3, 4]>(flatten([1, [2, [3, [4]]]] as [1, [2, [3, [4]]]])); +expectType(flatten([[[1]]] as number[][][])); + +// a tuple holding a plain array still flattens through it +expectType<(string | number)[]>(flatten([[[1]], 'x'] as [number[][], string])); diff --git a/test/flip.test.ts b/test/flip.test.ts index 8133a9b5..ce79c66d 100644 --- a/test/flip.test.ts +++ b/test/flip.test.ts @@ -4,3 +4,6 @@ import { flip, lt } from '../es'; expectType(flip(lt)(2, 1)); expectType(flip(lt)(2)(1)); + +// only the first two parameters swap; the rest keep their order +expectType(flip((a: number, b: string, c: boolean) => 'r')('x', 1, true)); diff --git a/test/mergeDeepLeft.test.ts b/test/mergeDeepLeft.test.ts new file mode 100644 index 00000000..3baee50e --- /dev/null +++ b/test/mergeDeepLeft.test.ts @@ -0,0 +1,8 @@ +import { expectType } from 'tsd'; + +import { mergeDeepLeft } from '../es'; + +expectType<{ a: { b: number; c: number } }>(mergeDeepLeft({ a: { b: 1 } }, { a: { c: 2 } })); + +// the left side wins on conflict, at any depth +expectType<{ a: { b: number } }>(mergeDeepLeft({ a: { b: 1 } }, { a: { b: 'x' } })); diff --git a/test/mergeDeepRight.test.ts b/test/mergeDeepRight.test.ts new file mode 100644 index 00000000..b42d8f60 --- /dev/null +++ b/test/mergeDeepRight.test.ts @@ -0,0 +1,12 @@ +import { expectType } from 'tsd'; + +import { mergeDeepRight } from '../es'; + +// nested objects are merged, not replaced wholesale as mergeRight would +expectType<{ a: { b: number; c: number } }>(mergeDeepRight({ a: { b: 1 } }, { a: { c: 2 } })); + +// the right side wins on conflict, at any depth +expectType<{ a: { b: string } }>(mergeDeepRight({ a: { b: 1 } }, { a: { b: 'x' } })); + +// built-ins are values to be replaced, not objects to recurse into +expectType<{ a: { x: number } }>(mergeDeepRight({ a: new Date() }, { a: { x: 1 } })); diff --git a/test/mergeLeft.test.ts b/test/mergeLeft.test.ts index 6f8882ad..eb910d27 100644 --- a/test/mergeLeft.test.ts +++ b/test/mergeLeft.test.ts @@ -7,3 +7,9 @@ const foo2 = { foo: 2 }; expectType<{ foo: string; bar: string; }>(mergeLeft(foo, bar)); expectType<{ foo: string; }>(mergeLeft(foo, foo2)); + +declare const optional: { a?: number, z: 1 }, required: { a: string, y: 2 }; + +// an optional key on the left may or may not be there, so it unions with the right rather than +// shadowing it outright +expectType<{ a: string | number, z: 1, y: 2 }>(mergeLeft(optional, required)); diff --git a/test/nAry.test.ts b/test/nAry.test.ts new file mode 100644 index 00000000..5972d11e --- /dev/null +++ b/test/nAry.test.ts @@ -0,0 +1,17 @@ +import { expectType } from 'tsd'; + +import { nAry } from '../es'; + +const f = (a: number, b: string, c: boolean) => 1; + +expectType(nAry(2, f)(1, 'x')); +expectType(nAry(0, f)()); + +const rest = (a: number, ...others: string[]) => true; + +expectType(nAry(3, rest)(1, 'x', 'y')); + +declare const n: number; + +// a non-literal arity yields no parameters at all rather than diverging +expectType(nAry(n, f)()); diff --git a/test/unnest.test.ts b/test/unnest.test.ts new file mode 100644 index 00000000..7bca61cb --- /dev/null +++ b/test/unnest.test.ts @@ -0,0 +1,7 @@ +import { expectType } from 'tsd'; + +import { unnest } from '../es'; + +// removes exactly one level, unlike flatten +expectType(unnest([[[1]]] as number[][][])); +expectType<[1, 2, [3]]>(unnest([[1], [2, [3]]] as [[1], [2, [3]]])); diff --git a/test/zipObj.test.ts b/test/zipObj.test.ts index ff0d74f2..dfcb2a91 100644 --- a/test/zipObj.test.ts +++ b/test/zipObj.test.ts @@ -30,3 +30,6 @@ expectType<{string: string, number: number}>(pipe( (a: [string, number]) => a, zipObj(['string', 'number']) )(['a', 42])); + +// where a key repeats, the first occurrence wins +expectType<{a: 1}>(zipObj(['a', 'a'], [1, 2])); diff --git a/types/util/_internal.d.ts b/types/util/_internal.d.ts index 16114d0d..05a701e7 100644 --- a/types/util/_internal.d.ts +++ b/types/util/_internal.d.ts @@ -30,22 +30,20 @@ export type Primitive = boolean | string | number | bigint | symbol | undefined /** * Take the first `N` elements of list `L`, positionally: the result is always exactly `N` long, * padding with `undefined` past the end of `L` and repeating `L`'s rest element where it has one. - * A non-literal `N` yields `[]`. + * A non-literal `N` yields `[]`, since `Acc` starts out empty and `0 extends number`. */ export type TakeFirst = - number extends N - ? [] - : Acc['length'] extends N - ? Acc - : L extends readonly [infer H, ...infer T] - ? TakeFirst - : number extends L['length'] - ? TakeFirst - : L extends readonly [] - ? TakeFirst - : L extends readonly [(infer H)?, ...infer T] - ? TakeFirst - : TakeFirst; + Acc['length'] extends N + ? Acc + : L extends readonly [infer H, ...infer T] + ? TakeFirst + : number extends L['length'] + ? TakeFirst + : L extends readonly [] + ? TakeFirst + : L extends readonly [(infer H)?, ...infer T] + ? TakeFirst + : TakeFirst; /** * Swap the first two elements of a list, leaving the rest in place. Used by `flip`. @@ -65,16 +63,22 @@ type _UnNest = L extends readonly [infer H, ...infer T] ? [...(H extends AnyList ? H : [H]), ..._UnNest] : []; +/** Invariant type equality - `A` and `B` are the same type, not merely mutually assignable. */ +type Equals = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; + /** * Flatten list `L` all the way down. + * + * This has to un-nest one level at a time until the result stops changing, rather than recursing + * structurally into each element. A tuple can hold a non-tuple array (`[number[][], string]`), + * and un-nesting that degrades the whole thing to a plain array - which then still has levels + * left to remove. Recursing per element instead both stops early on the array and drops it. */ -export type Flatten = number extends L['length'] - ? UnNest - : _Flatten; +export type Flatten = _Flatten; -type _Flatten = L extends readonly [infer H, ...infer T] - ? [...(H extends AnyList ? _Flatten : [H]), ..._Flatten] - : []; +type _Flatten = Equals extends true + ? L + : _Flatten, L>; /** * Build an object from a list of keys and a list of values, pairing them by position. Keys past