I've been trying to get these types working for a while now. Don't worry about the implementation. I only want to fix the types. The function takes and reduces over the config array using the fn
property. Each config item should have the aggregated return type from the previous steps fn
function. The types work, but the autocomplete in the editor is broken when I create a new object in the array, TS Server thinks it's an array type. Can this be fixed?
Code:
// interfaces
interface Config<T extends object, R extends object> {
fn: (state: T) => R
bro?: boolean
}
interface OverloadRunner {
<A extends object = object, B extends object = object>(
configArray: [Config<A, B>],
): void
<
A extends object = object,
B extends object = object,
C extends object = object,
>(
configArray: [Config<A, B>, Config<A & B, C>],
): void
<
A extends object = object,
B extends object = object,
C extends object = object,
D extends object = object,
>(
configArray: [Config<A, B>, Config<A & B, C>, Config<A & B & C, D>],
): void
// extend for further items
}
// function
const overloadRunner: OverloadRunner = (steps: Config<any, any>[]) => {
let accumulator = {}
for (const step of steps) {
const result = step.fn(accumulator)
accumulator = {
...accumulator,
...result,
}
}
}
// Usage example
overloadRunner([
{
fn: (state) => {
return { now: 'here' }
},
bro: false,
},
{
fn: (state) => {
return { next: 'step' } // state type correctly inferred as { now: string }
},
bro: false,
},
{
// does not autocomplete here. TS Server thinks it's an array
}
])
Screenshot of incorrect autocomplete: vscode type autocomplete
I tried moving the generic declarations to the entire interface and not the overloads. This looses the piping aspect of the state
arg
interface OverloadRunner<
A extends object = object,
B extends object = object,
C extends object = object,
D extends object = object,
> {
(configArray: [Config<A, B>]): void
(configArray: [Config<A, B>, Config<A & B, C>]): void
(configArray: [Config<A, B>, Config<A & B, C>, Config<A & B & C, D>]): void
}
I've tried using some tuple utility types, but they also didn't fix autocomplete.
type StepsArray<
T extends any[],
R extends object = RequestPropertiesAndHelpers,
> = T extends [infer First extends object, ...infer Rest]
? [Step<R, First>, ...StepsArray<Rest, R & First>]
: []
Given your particular example, you could make some of the elements of the configArray
parameter optional:
interface OverloadRunner {
<
A extends object = object,
B extends object = object,
C extends object = object,
D extends object = object,
>(
configArray: [Config<A, B>, Config<A & B, C>?, Config<A & B & C, D>?],
): void
}
That way if you call an OverloadRunner
with fewer than the maximum number of elements, the call will still succeed. Note that inference of the return type of the missing elements will fail, and fall back to the default type argument of object
:
overloadRunner([
{ fn(x) { return { a: 1 } } },
{ fn(x) { return { b: x.a } } }
])
// overloadRunner<object, {a: number}, {b: number}, object>(⋯)
overloadRunner([
{ fn(x: { a: number }) { return { b: x.a.toFixed() } } },
])
// overloadRunner<{a: number}, {b: string}, object, object>(⋯)
overloadRunner([
{ fn(x: { a: number }) { return { b: x.a.toFixed() } } },
{ fn(x) { return { c: x.b.endsWith("2") } } },
{ fn(x) { return { d: x.c ? new Date() : undefined } } }
])
// overloadRunner<{a: number}, {b: string}, {c: boolean}, {d: Date | undefined}>(⋯)
And because there's just a single call signature (so not overloaded, which means maybe the name is inapplicable now), the IntelliSense should be more straightforward.
Note that while you could write a call signature which would support an arbitrary number of elements of configArray
, you'd probably get bad inference and IntelliSense, where the contextual type of the callback parameters would fail. (This is the subject of microsoft/TypeScript#47599) But it seems from the question that you're okay picking some reasonably modest maximum number of elements, though, so I guess that won't be an issue.