1

What are the rules that govern how Typescript infers generic types in function parameters?

The generic type is not being correctly inferred in the code below - why?

//* main type  ******************************************************************************************/
type FunctionChainArray_<
  T extends [AnyFn, ...AnyFn[]],
  Parent extends AnyFn | never = never,
  Current extends AnyFn = T[0],
  Rest extends [AnyFn, ...AnyFn[]] = T extends [AnyFn, ...infer R extends [AnyFn, ...AnyFn[]]]
    ? R
    : never,
> = {
  singleFirstValue: [Current]
  startOfChain: [Current, ...FunctionChainArray_<Rest, Current>]
  chain: [(Input: ReturnType<Parent>) => ReturnType<Current>, ...FunctionChainArray_<Rest, Current>]
  last: [(Input: ReturnType<Parent>) => ReturnType<Current>]
}[[Parent] extends [never]
  ? [Rest] extends [never]
    ? 'singleFirstValue' // first value has no Rest
    : 'startOfChain' // build chain
  : [Rest] extends [never] // mid chain case
  ? 'last' // last value
  : 'chain'] // build chain

type FunctionChainArray<
  T extends readonly AnyFn[],
  TU extends AnyFn[] = UnwrapAsConstArray<T, AnyFn[]>, // remove readonly
  TP extends [AnyFn, ...AnyFn[]] = TU extends [AnyFn, ...AnyFn[]] ? TU : never,
> = IsFinite<
  TU,
  FunctionChainArray_<TP>,
  TU extends ((input: infer I) => any)[] ? ((input: I) => I)[] : [never]
>

//* compose function  ***************************************************************************************/
const compose = <T extends readonly AnyFn[]>(...fnsToAdd: FunctionChainArray<T>) => {
  const composedFn = fnsToAdd.reduce(
    (previousFn, currentFn) => (input) => currentFn(previousFn(input)),
  )
  return composedFn // as unknown as { T: T; FChain: FunctionChainArray<T> }
}

//* used function  ***************************************************************************************/
const logger = <T extends unknown>(str: string, rv: T) => { console.log(str); return rv }

const chainArray = [
  (a: 'a') => logger(`A:${a}`, 'Ra' as unknown as 'Ra'),
  (b: 'Ra') => logger(`B:${b}`, 'Rb' as unknown as 'Rb'),
  (c: 'Rb') => logger(`C:${c}`, 'Rc' as unknown as 'Rc'),
  (d: 'Rc') => logger(`D:${d}`, 'd' as unknown as 'd'),
] as const

// without inference, when the type is supplied it is correctly typecheck
const works = compose<typeof chainArray>(
  (a: 'a') => logger(`A:${a}`, 'Ra' as unknown as 'Ra'),
  (b: 'Ra') => logger(`B:${b}`, 'Rb' as unknown as 'Rb'),
  (c: 'error') => logger(`C:${c}`, 'Rc' as unknown as 'Rc'),
  (d: 'Rc') => logger(`D:${d}`, 'd' as unknown as 'd'),
)

// when one infer's the type it doesn't work
const doesntWork = compose(
  (a: 'a') => logger(`A:${a}`, 'Ra' as unknown as 'Ra'),
  (b: 'Ra') => logger(`B:${b}`, 'Rb' as unknown as 'Rb'),
  (c: 'error') => logger(`C:${c}`, 'Rc' as unknown as 'Rc'),
  (d: 'Rc') => logger(`D:${d}`, 'd' as unknown as 'd'),
)

code

TrevTheDev
  • 2,616
  • 2
  • 18
  • 36
  • 1
    Is that the most minimal example you can give? It looks like a *lot* of rather fiddly code, (with errors seemingly unrelated to your question). I'm not up to slogging through that at the moment; maybe after I get a full night's sleep I'll be more amenable to it, but I'd suggest in the meantime that you try to cut it down to something more manageable if at all possible. Good luck! – jcalz Sep 27 '22 at 02:28
  • @jcalz It looks worse than it actually is as I've included illustrative code. All errors are directly related to the question - with reduce not correctly detecting the type due to the same problem. I'll create a simpler version without the illustrative code and post it asap. – TrevTheDev Sep 27 '22 at 03:52
  • @jcalz [here](https://tsplay.dev/wXjb9m) is a simpler version. – TrevTheDev Sep 27 '22 at 04:02
  • 1
    Pipe functions are probably impossible to type in TypeScript, if you're going for this kind of "pipe". You'll either have to use a trillion overloads like RxJS or you'll need to redesign the API. Or, you could have a pseudo-typed pipe function that fails in lots of edge cases. – kelsny Sep 27 '22 at 04:30
  • @caTS - I've actually got it working for an aysnc pipe function and it's infinite without the overloads! A key difference between that and this, is in that case the generic must be specified for each item added to the pipe and so is not inferred. However here I'm trying to infer based on the array. You can see that it works if I supply the generic - and it'll work infinitely, however I have not been able to get working is the inference. But in theory it should also work ... – TrevTheDev Sep 27 '22 at 05:38
  • You have asked two questions here; you should pick one to be primary and remove the other (or make it obvious that it is an optional portion of the question). If your primary question is "what are the rules that govern..." then it is probably too broad and a full answer would likely be many pages long, and you will invite multiple answers that each mention different rules. – jcalz Sep 27 '22 at 15:26
  • 1
    If your primary question is "why is the type not being inferred correctly in the following code...", then this really doesn't seem like a [mre]. You could almost certainly present a *simpler* case that fails to infer in the same way. The gist of the answer here would be that it's just too complicated to infer; there is no mechanism by which TS can infer from a recursive conditional type. – jcalz Sep 27 '22 at 15:37
  • 1
    But I don't think I'm going to try to write up an answer here because it just isn't rewarding enough for me to fully debug someone else's complicated typings and dig through the compiler to see where it's losing. If you were asking how to *write* a `compose()` function that infers the way you want, then I might be more interested, but I'm pretty sure I've done this before and never found anything better than [this](https://stackoverflow.com/a/53175538/2887218) – jcalz Sep 27 '22 at 15:41
  • 1
    @jcalz - thank you so much again for your input - you have solved my problem - the code you linked contained this gem `(...f: F & AsChain)` and that was the only change required to make my code work! My original question goes to the heart of that solution. It would be useful if somewhere that was documented - as others will run into the same problem, and likely my lack of understanding of this will cause me other problems in the future too! I'm not putting this on you to do, only wishful commenting - but again thank you for your valuable input. – TrevTheDev Sep 28 '22 at 02:10
  • 1
    @jcalz - also thank you for the comments on recursion - and the solution you provided will be more performant. – TrevTheDev Sep 28 '22 at 02:17

0 Answers0