2

I have an array of instances where execute returns a type, here are for example 2 classe whose instances are in the array:

class ClassBase<TReturn>
{
   execute (): TReturn
}

class ReturnsString extends ClassBase<string>
{
   execute () : string
}

class ReturnsNumber extends ClassBase<number>
{
   execute () : number
}

const items:[ReturnsString,ReturnsNumber] = [new ReturnsString(), new ReturnsNumber()];

inside a method i will call execute in a loop for each item in tuple.

How to declare a method that

  • accepts items - items could be of any length and can contain various types, eg. could be [ReturnString] or [ReturnString,ReturnString,ReturnString,ReturnNumber,....]

  • returns type of the returning type tuple [string,string,string,number]

E_net4
  • 27,810
  • 13
  • 101
  • 139
Ivan
  • 1,254
  • 12
  • 25

3 Answers3

3

You can use mapped types to map over a tuple and extract the return type for each tuple item:

type ReturnsOfClassBase<T extends Record<number, ClassBase<any>>> = {
  -readonly [P in keyof T] : T[P] extends ClassBase<infer R> ? R: never
}
function getReturns<T extends readonly ClassBase<any>[]>(p: T): ReturnsOfClassBase<T> {
  const result = []
  for(let r of p) {
    result.push(r.execute());
  }

  return result as any
}

let r = getReturns(items)

Playground Link

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • I tried this solution, but it still returns a tuple of union. What the question wants is to return a tuple type matching the length and order of the input tuple. – CMCDragonkai Jan 17 '22 at 05:56
  • @CMCDragonkai If you check the playground link, you'll see that `r` is `[string, number]` so it does work for the given example. If something is not working for you ask another question and I can have a look – Titian Cernicova-Dragomir Jan 17 '22 at 05:58
  • I did check, and I ran my own example. But I noticed your one has `as const` for the `items`. Without that, it goes back to array of union. – CMCDragonkai Jan 17 '22 at 06:00
  • What is the `as const` doing? And it's not mentioned in your answer. – CMCDragonkai Jan 17 '22 at 06:00
  • @CMCDragonkai This answer assumes that the parameter ( in this case `items` ) is already a tuple. If you want `T` to be inferred as a tuple when you call the function with `getReturns( [new ReturnsString(), new ReturnsNumber()])`, you can use `T extends (readonly [ClassBase] | readonly ClassBase[])` as a constraint instead. – Titian Cernicova-Dragomir Jan 17 '22 at 06:03
  • So the reason is that if just do `const items = [...];`, TS infers it to be an array, and when it's an array, the mapped type becomes a tuple/array of union. So the `as const` is a way of forcing TS to recognise it as a tuple. – CMCDragonkai Jan 17 '22 at 06:08
  • @CMCDragonkai Yes. That is right, if the input is an array of union, the output will be an array. If it is a tuple the output is a tuple. By default TS infers arrays not tuples, `as const` makes it infer a readonly tuple – Titian Cernicova-Dragomir Jan 17 '22 at 06:12
1

I recently implemented a similar types:

type TransformTuple<Tuple extends (() => any)[], Returns extends any[] = []> =
  Tuple extends [() => any, ...infer More] ?
    More extends (() => any)[] ?
      TransformTuple<More, [...Returns, ReturnType<Tuple[0]>]>
      : never
    : Returns

type Test = TransformTuple<[() => 1, () => "2"]>
/*
type Test = [1, "2"]
*/

//NOTE Only tuples are supported

//If use normal array like this:
type Test2 = TransformTuple<Array<() => "never">>
/*
//You only get an empty array
type Test2 = []
*/
justTryIt
  • 67
  • 5
0

nice question! It made me think a lot and use many advanced TS typing features, but I guess that this makes what you are looking for:

abstract class ClassBase<TReturn = any>
{
   abstract execute (): TReturn
}

type FlattenIfArray<T> = T extends (infer R)[] ? R : T
type ExtendsOf<T> = T extends ClassBase<infer R> ? R : T
type ExecuteArrayReturn<T> = ExtendsOf<FlattenIfArray<T>>

function example<T extends ClassBase<ExecuteArrayReturn<T>>[]>(items: T): ExecuteArrayReturn<T> {
    return items.map(item => item.execute()) as any
}

class ReturnsString extends ClassBase<string>
{
   execute () : string {return 'a'}
}

class ReturnsNumber extends ClassBase<number>
{
   execute () : number {return 1}
}

const items:[ReturnsString,ReturnsNumber] = [new ReturnsString(), new ReturnsNumber()];

const result = example(items);

I needed to create some types to extract the execute function return value from instances of the ClassBase heirs. FlattenIfArray gets the type of the array items, ExtendsOf get the type of the generic type that has been set to T and ExecuteArrayReturn join both of them.

When you implement your real function, don't forget about the as any on the function return. Otherwise, TS will think that you want to return an array of the provided items types union, for example: (string | number)[].

Pedro Mutter
  • 1,178
  • 1
  • 13
  • 18