0

I am mapping an object of type A multiple times via a succession of functions.
For example:

  1. first a function with type (item: A) => B
  2. then a function with type (item: B) => C
  3. then a function with type (item: C) => D

and out comes an object of type D. These functions are passed as an array, which in the example would have type [(item: A) => B, (item: B) => C, (item: C) => D].

An concrete example of this:

const mappings: MappingSequence<number, { duration: string }> = [
  a => a * 60,            // (a: number) => number
  b => `${b} seconds`,    // (b: number) => string
  c => ({ duration: c }), // (c: string) => { duration: string }
];

where applying all 3 functions to the number 2 results in { duration: '120 seconds' }.

I am looking to express this generic type MappingSequence<A, Z>:

type Mapping<X, Y> = (item: X) => Y;

// This definition style won't work because there are infinitely many options
type MappingSequence<A, Z> =
  [] |                                            // 0-step mapping, only if A = Z
  [Mapping<A, Z>] |                               // 1-step mapping
  [Mapping<A, B>, Mapping<B, Z>] |                // 2-step mapping, for any B
  [Mapping<A, B>, Mapping<B, C>, Mapping<C, Z>] | // 3-step mapping, for any B and C
  // etc., for arbitrary array lengths

// This style might work, but the recursion is tricky
type MappingSequence<A, Z> =
  [Mapping<A, Z>] |
  [Mapping<A, B>, ...MappingSequence<B, Z>]; // for any B

However, I've gotten stuck in multiple different attempts. The key difficulty seems to be expressing "for any B". I was looking to do this with the infer keyword; however, then I need to have conditional types, which I found hard when having the generic over A and Z.

Any insights in how this can be achieved? Thanks in advance.

2
  • can you show some example inputs and outputs? If you say "for arbitrary array lengths", that means the union as shown above would have an infinite amount of elements, which would be impossible. Commented Jun 26, 2022 at 15:31
  • @TobiasS. I have added an example to the question. My fruitless attempt mainly shows that a union for each length is not the right way to construct this (and one of the reasons I got stuck). Commented Jun 26, 2022 at 19:08

1 Answer 1

1

This seems to be possible to achieve when using a generic function:

type DecrementTable = [
  -1, 0, 1, 2, 3, 4, 5,
  6, 7, 8, 9, 10, 11, 12,
  13, 14, 15, 16, 17, 18,
  19, 20, 21, 22, 23, 24,
  25, 26, 27, 28, 29, 30,
  31, 32, 33, 34, 35, 36,
  37, 38, 39, 40, 41, 42,
  43, 44, 45, 46, 47, 48,
  49, 50
]

type Decrement<T extends number | string> = DecrementTable[T & keyof DecrementTable]

type MappingReturnType<T extends any[]> = T extends [...any, infer R] ? R : never

function mappingSequence<
  S, 
  F extends any[]
>(startValue: S, fn: [...{ 
    [K in keyof F]: (args: K extends "0" ? S : F[Decrement<K & string> & keyof F]) => F[K] 
  }]) : MappingReturnType<F> {
    
  return {} as any
}

Let's see if this works.

const result = mappingSequence(23, [
  (a: number) => a * 60,            
  (b: number) => `${b} seconds`,    
  (c: string) => ({ duration: c }),
])
// const result: {
//     duration: string;
// }

As you can see there is a small caveat: You have to give each function argument an explicit type. But TypeScript will give an error if the given type is incorrect.

const result = mappingSequence(23, [
  (a: number) => a * 60,            
  (b: string) => `${b} seconds`, // Error: '(b: string) => string' is not assignable to type '(args: number) => string'    
  (c: string) => ({ duration: c }),
])

Playground

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, that's getting close. What is the blocker that makes us need a function rather than being able to type the array directly? Also wonder if we can get around the DecrementTable. Good inspiration in any case!
Well, we need a function since stand alone types are simply not powerfull enough. With a stand-alone type you have only one option: Creating a union of all possible combinations. Since this union would be of infinite size, this is not an option here. Only generic functions are capable of validating such stuff since you can use arguments to infer types which then can be used for other generic types with complex logic. With stand-alone types, it is generally impossible to express a relation between different parts of a type, which is what we need here.
TypeScript does not offer any option to decrement numbers out of the box. The DecrementTable offers a good and fast solution to emulate decrementing numbers. There are other options where tuples are recursively tailed to decrement numbers too, but these are slower and also reach the recursion limit of TypeScript quite fast.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.