Skip to content

Commit 79c16a1

Browse files
committed
chore@small
1 parent 6350a85 commit 79c16a1

2 files changed

Lines changed: 349 additions & 42 deletions

File tree

plan.md

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,53 @@
1-
# Comparison Plan: Rambda vs Remeda TypeScript Typings
1+
# Comparison Plan: Rambda vs Radashi & Remeda TypeScript Typings
22

33
## Objective
4-
Identify methods in Rambda where TypeScript typings are less precise or less ergonomic than in Remeda, and demonstrate these differences using test cases.
5-
6-
## Selected Candidate Methods
7-
Based on initial research, the following methods are candidates for comparison:
8-
9-
1. **`map`**:
10-
- **Remeda**: Preserves tuple length and structure; preserves `readonly` status.
11-
- **Rambda**: Often collapses tuples to arrays; forces mutability in output.
12-
13-
2. **`indexBy`**:
14-
- **Remeda**: Supports literal keys and return `Partial` records for safety.
15-
- **Rambda**: Returns `Record<string, T>`, losing key literal information and implying all keys exist.
16-
17-
3. **`filter`**:
18-
- **Remeda**: Strong support for type guards and complex predicate narrowing, especially in `pipe`.
19-
- **Rambda**: Has type guard support but might have edge cases with `Boolean` constructor and deep pipes.
20-
21-
4. **`pick` / `omit`**:
22-
- **Remeda**: Strict key checking and excellent inference in data-first usage.
23-
- **Rambda**: Uses complex path-based typings which can sometimes be brittle.
24-
25-
## Investigation Steps
26-
27-
### Phase 1: Environment Setup
28-
- Ensure `remeda` is available for typing comparisons (or copy relevant type definitions).
29-
- Create a comparison test suite (e.g., `source/remeda-comparison-spec.ts`).
30-
31-
### Phase 2: Empirical Testing
32-
For each selected method:
33-
1. Write a test case using **Rambda** that demonstrates a typing limitation (e.g., losing tuple length).
34-
2. Write an equivalent test case using **Remeda** (or its types) that succeeds in the same scenario.
35-
3. Use `expectTypeOf` to document the exact differences.
36-
37-
### Phase 3: Analysis and Reporting
38-
- Summarize findings for each method.
39-
- Propose specific typing improvements for Rambda based on Remeda's approach.
40-
- Prove that the proposed types (if applicable) fix the issues.
41-
42-
## Expected Outcomes
43-
- A new test file `source/remeda-comparison-spec.ts` demonstrating the findings.
44-
- A detailed report of better typings found in Remeda.
4+
Identify methods in Rambda where TypeScript typings differ from Radashi and Remeda, and demonstrate differences with runnable type tests in the style of the existing `source/remeda-comparison-spec.ts`.
5+
6+
## Phase 1: Research (complete)
7+
8+
### Radashi (v13.0.0-beta)
9+
- **Data-first** (not curried) — functions take data as first arg, unlike Rambda's data-last curry.
10+
- **Different naming**: `group` (not `groupBy`), `objectify` (not `indexBy`), `sort` (not `sortBy`), `select` (combines filter+map), `selectFirst` (find+map), `chain` (not `pipe`).
11+
- **No `filter`/`find`/`drop`/sync `map`** — Radashi uses `select`/`selectFirst` which lack type guard narrowing.
12+
- **No variadic tuple inference**`chain` uses 10 overloads (Rambda `pipe` uses 20).
13+
- **`const` type parameters** on `first`/`last`/`draw` for literal tuple extraction.
14+
- `group` returns `Partial<Record<K, T[]>>` (correct, same as Rambda `groupBy`).
15+
- `objectify` returns `Record<Key, Value>` (non-partial — assumes all keys exist; Rambda `indexBy` returns `Record<string, T>`).
16+
- `sort` only accepts **numeric** getter (Rambda `sortBy` accepts any `Ord` = `number | string | boolean | Date`).
17+
- `sum` has 2 overloads: `(array: number[]) => number` and `(array: T[], fn) => number` (Rambda only has `(list: number[]) => number`).
18+
- `unique` supports optional `toKey` function (Rambda `uniq` does not; `uniqBy` exists separately).
19+
- `pick` supports both key array and predicate filter (Rambda supports string-path and key array).
20+
21+
### Remeda (v2.34.1)
22+
- Already compared in `source/remeda-comparison-spec.ts` across 6 method categories.
23+
- Remeda advantages confirmed: type guard narrowing, tuple preservation, multi-criteria sortBy, literal key inference, `NonEmptyArray` for groupBy.
24+
- Remaining candidates needing comparison: `pick` (strict key inference), `omit`, `uniq`, `zip`, `sum`.
25+
26+
## Phase 2: Radashi Comparison Tests (`source/radashi-comparison-spec.ts`)
27+
28+
| # | Rambda method | Radashi equivalent | Key type difference |
29+
|---|---|---|---|
30+
| 1 | `find` | `selectFirst` | Radashi `selectFirst` has no type guard overload; `condition` is `(item, idx) => boolean` |
31+
| 2 | `filter` | `select` | Radashi `select` combines filter+map; no type guard; `condition` is `(item, idx) => boolean` |
32+
| 3 | `groupBy` | `group` | Both return `Partial<Record<K, T[]>>` — comparable |
33+
| 4 | `indexBy` | `objectify` | Radashi infers literal keys (`Record<K, V>`) but non-partial vs Rambda `Record<string, T>` |
34+
| 5 | `sortBy` | `sort` | Radashi only numeric getter, Rambda accepts any `Ord` |
35+
| 6 | `pick` | `pick` | Radashi supports predicate filter (`KeyFilter`); Rambda has string-path overload |
36+
| 7 | `omit` | `omit` | Both similar `Omit<T, K>` |
37+
| 8 | `uniq` | `unique` | Radashi supports optional `toKey` (like `uniqBy`); Rambda has separate `uniq`/`uniqBy` |
38+
| 9 | `sum` | `sum` | Radashi adds mapper overload `(T[], fn) => number` |
39+
| 10 | `pipe` | `chain` | Radashi 10 overloads vs Rambda 20; neither uses variadic tuples |
40+
| 11 | `zip` | `zip` | Radashi variadic (2-5 arrays, tuple-of-tuples return); Rambda curried `(K[]) => (V[]) => KeyValuePair[]` |
41+
42+
## Phase 3: Extend Remeda Comparison Tests (`source/remeda-comparison-spec.ts`)
43+
44+
Add comparisons for:
45+
- **`pick`**: Remeda's `PickFromArray<T, Keys>` strict key inference vs Rambda's `MergeTypes<Pick<T, K>>` + string-path.
46+
- **`omit`**: Remeda's strict key inference vs Rambda's.
47+
- **`uniq`**: Remeda's overloads (no args, key selector, etc.) vs Rambda's simple `uniq<T>(list: T[])`.
48+
- **`zip`**: Remeda's tuple inference vs Rambda's curried `KeyValuePair[]`.
49+
50+
## Phase 4: Verification
51+
52+
Run CI order: `yarn lint:typings``yarn test:ci``yarn test:typings`
53+
All new type assertions must pass.

source/radashi-comparison-spec.ts

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import {
2+
filter,
3+
find,
4+
groupBy,
5+
indexBy,
6+
omit,
7+
pick,
8+
pipe,
9+
sortBy,
10+
sum,
11+
uniq,
12+
zip,
13+
} from 'rambda'
14+
import { describe, expectTypeOf, it } from 'vitest'
15+
import {
16+
chain as radashiChain,
17+
group as radashiGroup,
18+
objectify as radashiObjectify,
19+
omit as radashiOmit,
20+
pick as radashiPick,
21+
select as radashiSelect,
22+
selectFirst as radashiSelectFirst,
23+
sort as radashiSort,
24+
sum as radashiSum,
25+
unique as radashiUnique,
26+
zip as radashiZip,
27+
} from 'radashi'
28+
29+
describe('Radashi vs Rambda: API design', () => {
30+
it('Radashi is data-first, Rambda is curried data-last', () => {
31+
const items = [3, 1, 2]
32+
33+
// Radashi: data-first, all args at once
34+
const radashiResult = radashiSort(items, x => x)
35+
expectTypeOf(radashiResult).toEqualTypeOf<number[]>()
36+
37+
// Rambda: curried data-last, designed for pipe
38+
const rambdaResult = pipe(items, sortBy(x => x))
39+
expectTypeOf(rambdaResult).toEqualTypeOf<number[]>()
40+
})
41+
})
42+
43+
describe('Radashi vs Rambda: selectFirst vs find (type guard narrowing)', () => {
44+
it('Rambda find supports type guard narrowing; Radashi selectFirst does not', () => {
45+
const items = ['hello', 'world', 42] as (string | number)[]
46+
47+
const radashiResult = radashiSelectFirst(
48+
items,
49+
x => String(x),
50+
(x): x is string => typeof x === 'string',
51+
)
52+
// Radashi condition is typed as (item: T, index: number) => boolean
53+
// No type guard overload — result is still string | undefined
54+
// because mapper returns string for all T, and condition narrows runtime only
55+
expectTypeOf(radashiResult).toEqualTypeOf<string | undefined>()
56+
57+
const rambdaResult = pipe(
58+
items,
59+
find((x): x is string => typeof x === 'string'),
60+
)
61+
// Rambda find has type guard overload: predicate(value is S) => S | undefined
62+
expectTypeOf(rambdaResult).toEqualTypeOf<string | undefined>()
63+
})
64+
})
65+
66+
describe('Radashi vs Rambda: select vs filter (type guard narrowing)', () => {
67+
it('Rambda filter supports type guard narrowing; Radashi select does not', () => {
68+
interface Foo { a: number }
69+
interface Bar extends Foo { b: string }
70+
const items = [{ a: 1 }, { a: 2, b: 'x' }] as (Foo | Bar)[]
71+
72+
const radashiResult = radashiSelect(
73+
items,
74+
x => x,
75+
(x): x is Bar => 'b' in x,
76+
)
77+
// Radashi condition is (item: T, index: number) => boolean — no narrowing
78+
// Result type is (Foo | Bar)[] because condition doesn't narrow
79+
expectTypeOf(radashiResult).toEqualTypeOf<(Foo | Bar)[]>()
80+
81+
const rambdaResult = pipe(
82+
items,
83+
filter((x): x is Bar => 'b' in x),
84+
)
85+
// Rambda filter supports type guard → Bar[]
86+
expectTypeOf(rambdaResult).toEqualTypeOf<Bar[]>()
87+
})
88+
})
89+
90+
describe('Radashi vs Rambda: group vs groupBy (partial records)', () => {
91+
it('Both return Partial<Record<K, T[]>>', () => {
92+
const items = [
93+
{ category: 'a', val: 1 },
94+
{ category: 'b', val: 2 },
95+
{ category: 'a', val: 3 },
96+
]
97+
98+
const radashiResult = radashiGroup(items, x => x.category)
99+
// Radashi: { [K in Key]?: T[] } — equivalent to Partial<Record<string, T[]>>
100+
expectTypeOf(radashiResult).toEqualTypeOf<
101+
Partial<Record<string, { category: string; val: number }[]>>
102+
>()
103+
104+
const rambdaResult = pipe(items, groupBy(x => x.category))
105+
// Rambda: Partial<Record<K, T[]>> where K extends string
106+
expectTypeOf(rambdaResult).toEqualTypeOf<
107+
Partial<Record<string, { category: string; val: number }[]>>
108+
>()
109+
})
110+
111+
it('Both allow number/symbol keys via groupBy', () => {
112+
const items = [1, 2, 3, 4]
113+
114+
const radashiResult = radashiGroup(items, n => (n % 2 === 0 ? 'even' : 'odd'))
115+
expectTypeOf(radashiResult).toEqualTypeOf<
116+
Partial<Record<string, number[]>>
117+
>()
118+
})
119+
})
120+
121+
describe('Radashi vs Rambda: objectify vs indexBy (key inference)', () => {
122+
it('Radashi objectify infers literal keys; Rambda indexBy returns wide Record<string, T>', () => {
123+
const list = [
124+
{ id: 'a' as const, name: 'Alice' },
125+
{ id: 'b' as const, name: 'Bob' },
126+
]
127+
128+
// Radashi objectify: Record<Key, Value> with inferred literal keys
129+
const radashiResult = radashiObjectify(list, x => x.id)
130+
expectTypeOf(radashiResult).toEqualTypeOf<
131+
Record<'a' | 'b', { id: 'a' | 'b'; name: string }>
132+
>()
133+
134+
// Rambda indexBy: takes property key string, returns Record<string, T>
135+
const rambdaResult = indexBy('id')(list)
136+
expectTypeOf(rambdaResult).toEqualTypeOf<
137+
Record<string, { id: 'a' | 'b'; name: string }>
138+
>()
139+
})
140+
141+
it('Radashi objectify supports value mapper', () => {
142+
const list = [
143+
{ id: 'a' as const, name: 'Alice' },
144+
{ id: 'b' as const, name: 'Bob' },
145+
]
146+
147+
// Radashi: objectify(array, getKey, getValue?) — can map values too
148+
const radashiResult = radashiObjectify(list, x => x.id, x => x.name)
149+
expectTypeOf(radashiResult).toEqualTypeOf<Record<'a' | 'b', string>>()
150+
151+
// Rambda: indexBy only indexes by property — no value mapping overload
152+
})
153+
})
154+
155+
describe('Radashi vs Rambda: sort vs sortBy (comparator types)', () => {
156+
it('Radashi sort only accepts numeric getter; Rambda sortBy accepts any Ord', () => {
157+
const items = [{ a: 2 }, { a: 1 }, { a: 0 }]
158+
159+
// Radashi sort: getter must return number
160+
const radashiResult = radashiSort(items, x => x.a)
161+
expectTypeOf(radashiResult).toEqualTypeOf<{ a: number }[]>()
162+
163+
// Radashi sort supports boolean desc flag
164+
const radashiDesc = radashiSort(items, x => x.a, true)
165+
expectTypeOf(radashiDesc).toEqualTypeOf<{ a: number }[]>()
166+
167+
// Rambda sortBy: getter returns any Ord (number | string | boolean | Date)
168+
const rambdaResult = pipe(items, sortBy(x => x.a))
169+
expectTypeOf(rambdaResult).toEqualTypeOf<{ a: number }[]>()
170+
})
171+
172+
it('Rambda sortBy supports string and Date comparators; Radashi sort does not', () => {
173+
const strItems = [{ name: 'Charlie' }, { name: 'Alice' }, { name: 'Bob' }]
174+
175+
const rambdaResult = pipe(strItems, sortBy(x => x.name))
176+
expectTypeOf(rambdaResult).toEqualTypeOf<{ name: string }[]>()
177+
178+
// Radashi sort: getter must return number — would error on string
179+
// @ts-expect-error — Radashi sort getter requires number return
180+
radashiSort(strItems, x => x.name)
181+
})
182+
})
183+
184+
describe('Radashi vs Rambda: pick (key inference)', () => {
185+
it('Radashi pick supports predicate filter; Rambda pick supports string-path', () => {
186+
const input = { a: 'foo', b: 2, c: 3 } as const
187+
188+
// Radashi pick with key array
189+
const radashiArray = radashiPick(input, ['a', 'c'])
190+
expectTypeOf(radashiArray).toEqualTypeOf<{ readonly a: 'foo'; readonly c: 3 }>()
191+
192+
// Radashi pick with predicate filter
193+
const radashiPredicate = radashiPick(input, (_value, key) => key !== 'b')
194+
expectTypeOf(radashiPredicate).toEqualTypeOf<{ readonly a: 'foo'; readonly c: 3 }>()
195+
196+
// Rambda pick with array
197+
const rambdaArray = pipe(input, pick(['a', 'c']))
198+
expectTypeOf(rambdaArray.a).toEqualTypeOf<'foo'>()
199+
200+
// Rambda pick with string-path
201+
const rambdaString = pipe(input, pick('a,c'))
202+
expectTypeOf(rambdaString.a).toEqualTypeOf<'foo'>()
203+
})
204+
})
205+
206+
describe('Radashi vs Rambda: omit', () => {
207+
it('Both infer remaining keys when omitting', () => {
208+
const input = { a: 'foo', b: 2, c: 3 } as const
209+
210+
const radashiResult = radashiOmit(input, ['b'])
211+
expectTypeOf(radashiResult).toEqualTypeOf<{ readonly a: 'foo'; readonly c: 3 }>()
212+
213+
const rambdaResult = pipe(input, omit(['b']))
214+
expectTypeOf(rambdaResult.a).toEqualTypeOf<'foo'>()
215+
})
216+
})
217+
218+
describe('Radashi vs Rambda: unique vs uniq', () => {
219+
it('Radashi unique supports optional toKey; Rambda uniq does not', () => {
220+
const items = [1, 2, 1, 3]
221+
222+
const rambdaResult = uniq(items)
223+
expectTypeOf(rambdaResult).toEqualTypeOf<number[]>()
224+
225+
// Radashi unique with optional toKey (similar to uniqBy but combined)
226+
const radashiResult = radashiUnique(items)
227+
expectTypeOf(radashiResult).toEqualTypeOf<number[]>()
228+
229+
// Radashi unique with toKey function
230+
const objItems = [{ id: 1 }, { id: 2 }, { id: 1 }]
231+
const radashiKeyed = radashiUnique(objItems, x => x.id)
232+
expectTypeOf(radashiKeyed).toEqualTypeOf<{ id: number }[]>()
233+
})
234+
})
235+
236+
describe('Radashi vs Rambda: sum', () => {
237+
it('Radashi sum has mapper overload; Rambda sum only accepts number[]', () => {
238+
const numbers = [1, 2, 3]
239+
240+
const radashiResult = radashiSum(numbers)
241+
expectTypeOf(radashiResult).toEqualTypeOf<number>()
242+
243+
const rambdaResult = sum(numbers)
244+
expectTypeOf(rambdaResult).toEqualTypeOf<number>()
245+
246+
// Radashi sum with mapper function
247+
const items = [{ value: 1 }, { value: 2 }]
248+
const radashiMapped = radashiSum(items, x => x.value)
249+
expectTypeOf(radashiMapped).toEqualTypeOf<number>()
250+
})
251+
})
252+
253+
describe('Radashi vs Rambda: zip', () => {
254+
it('Radashi zip is variadic multi-array; Rambda zip is curried pair', () => {
255+
const keys = ['a', 'b']
256+
const vals = [1, 2]
257+
258+
// Radashi zip: variadic, tuple-per-element return
259+
const radashiResult = radashiZip(keys, vals)
260+
expectTypeOf(radashiResult).toEqualTypeOf<[string, number][]>()
261+
262+
// Rambda zip: curried, returns KeyValuePair[]
263+
const rambdaResult = zip(keys)(vals)
264+
expectTypeOf(rambdaResult).toEqualTypeOf<[string, number][]>()
265+
})
266+
})
267+
268+
describe('Radashi vs Rambda: chain vs pipe (composition arity)', () => {
269+
it('Rambda pipe supports up to 20 operations; Radashi chain supports 10', () => {
270+
const add1 = (x: number) => x + 1
271+
const double = (x: number) => x * 2
272+
const toString = (x: number) => String(x)
273+
274+
// Radashi: chain data-first, overload-based (10 max)
275+
const radashiChained = radashiChain(add1, double, toString)
276+
expectTypeOf(radashiChained(0)).toEqualTypeOf<string>()
277+
278+
// Rambda: pipe data-last, variadic-tuple-based (20 overloads)
279+
const rambdaResult = pipe(0, add1, double, toString)
280+
expectTypeOf(rambdaResult).toEqualTypeOf<string>()
281+
})
282+
})
283+
284+
describe('Radashi vs Rambda: generic type utilities', () => {
285+
it('Radashi exposes Simplify, IsExactType, Falsy; Rambda does not export these', () => {
286+
// Radashi exports advanced type utilities:
287+
// Simplify<T> — flattens intersection types
288+
// IsExactType<A, B> — exact type equality check
289+
// Falsy — union of all falsy values
290+
// These are not exported from Rambda.
291+
// Test is structural: Radashi's types are ambient, verified at compile time.
292+
293+
// Radashi Falsy type = null | undefined | false | '' | 0 | 0n
294+
// Rambda has ExcludeFalsy in filter(Boolean) but doesn't export the type
295+
const radashiFalsy: (typeof import('radashi'))['Falsy'] = false as any
296+
expectTypeOf(radashiFalsy).not.toEqualTypeOf<true>()
297+
})
298+
})

0 commit comments

Comments
 (0)