Skip to content

Proposal: Introduce higher-kinded types for ArrayMap and ArrayReduce transformations #1186

@benzaria

Description

@benzaria

Hi! @sindresorhus I’d like to propose adding some types that simulate Array.map and Array.reduce behavior on tuple types in a reusable, higher-kinded style.

These utilities use a callback interface approach to allow extensible behavior via type “callbacks,” which makes it possible to create mapped/reduced outputs based on keys from a reusable callback map.

Down here are some basic implementation.

Example 1: ArrayMap

type ArrayMap<
    Array_ extends UnknownArray,
    Mapper extends MapCallBacks,
    Array__ extends UnknownArray = Array_,
    Index extends number = 0,
    Acc extends UnknownArray = []
> =
    Array_ extends readonly [infer Head, ...infer Tail]
        ? ArrayMap<Tail, Mapper, Array__, Increment<Index>, [...Acc,
            MapCallBack<Head, Index, Array__>[Mapper]
        ]>
        : Acc
;

Then users can define:

interface MapCallBack<V, I, T> {
    'curr->next': Increment<I> extends infer Next extends number
        ? T[Next] extends string
            ? `${V & string}->${T[Next]}`
            : never
        : never;
    'A + B': V extends readonly [infer A extends number, infer B extends number]
        ? Sum<A, B>
        : never;
    Snake: `__${V & string}__`;
    Kebab: `--${V & string}--`;
}

And use like:

const arr1 = ['foo', 'bar', 'baz'] as const
const fn1 = arr1.map(x => `__${x}__` ) as ArrayMap<typeof arr1, 'Snake'>
//      ^? ["__foo__", "__bar__", "__baz__"]

const arr2 = [[1, 2], [5, 10]] as const
const fn2 = arr2.map(([a, b]) => a + b) as ArrayMap<typeof arr2, 'A + B'>
//      ^? [3, 15]

type M = ArrayMap<typeof arr1, 'curr->next'>
//   ^? ["foo->bar", "bar->baz", never]

Example 2: ArrayReduce

type ArrayReduce<
    Array_ extends UnknownArray,
    Reducer extends ReduceCallBacks,
    Array__ extends UnknownArray = Array_,
    Index extends number = 0,
    Acc = never
> =
    Array_ extends readonly [infer Head, ...infer Tail]
        ? ArrayReduce<Tail, Reducer, Array__, Increment<Index>,
            ReduceCallBack<Acc, Head, Index, Array__>[Reducer]
        >
        : Acc
;

Then users can define:

interface ReduceCallBack<P, V, I, T> {
    Join: [P] extends [never] ? V : `${P & string}, ${V & string}`;
    Max: [P] extends [never] ? V
        : GreaterThanOrEqual<P, V > extends true ? P : V
    Min: [P] extends [never] ? V 
        : LessThanOrEqual<P, V > extends true ? P : V;
}

And use like:

type TupleMax<T extends readonly number[]> = ArrayReduce<T, 'Max'>

const arr3 = [1, 22, 8, 10, 5, 4] as const
const fn3 = arr3.reduce((x, y) => x >= y ? x : y) as TupleMax<typeof arr3>
//      ^? 22
const fn4 = arr3.reduce((x, y) => x <= y ? x : y) as ArrayReduce<typeof arr3, 'Min'>
//      ^? 1

type R = ArrayReduce<['foo', 'bar', 'baz'], 'Join'>
//	 ^? 'foo, bar, baz'

Motivation

  • This opens the door to abstracted, composable operations over tuple types without hardcoding logic into every single type.
  • Users could define their own MapCallBack/ReduceCallBack sets and re-use the ArrayMap/ArrayReduce types.

Would love to hear your thoughts! I can open a PR if this is something you'd consider.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions