The Setup
I have a lib that provides helper functions to dynamically grow and use an object, and an arbitrary set of files that use this helper to define a list of steps this object will go through:
// samplestepdefinition.ts
interface C {
c1: string,
c2: string
}
const isGreaterThanTwo = (number: number): boolean => number > 2
const steps = [
Steps.initialize('a', 3),
Steps.provide('b', 'a', isGreaterThanTwo),
Steps.provide('c', 'b', (b: boolean): C => ({ c1: 'foo', c2: b ? 'bar' : 'baz' })),
Steps.use('c.c2', console.log)
]
Steps.processSteps(steps) // logs 'bar'
// stepslib-simplified.ts
import lodash_get from 'lodash/fp/get'
import lodash_set from 'lodash/fp/set'
interface StepContext extends Object { }
type StepResolver = (context: StepContext) => StepContext // but the output is modified
type ProviderFn = (valueToUse: any) => any
type Provide = (keyToSet: string, keyToUse: string, fn: ProviderFn) => StepResolver
const provide: Provide = (keyToSet, keyToUse, fn) => (context) =>
lodash_set(keyToSet, fn(lodash_get(keyToUse, context)), context)
type Initialize = (keyToSet: string, value: any) => StepResolver
const initialize: Initialize = lodash_set
type UserFn = (valueToUse: any) => any
type Use = (keyToUse: string, fn: UserFn) => StepResolver
const use: Use = (keyToUse, fn) => context => fn(lodash_get(keyToUse, context))
const Steps = {
provide,
initialize,
use,
processSteps: (steps: StepResolver[]): void => {
steps.reduce((prevContext, step) => step(prevContext), {})
}
}
export default Steps
The Challenge
In this example, there is no actual type validation. If the type definition of isGreaterThanTwo
would take a string as argument, the code would not clash. Neither would replacing step 3 with Steps.provide('c', 'b', (b: number): [...]
.
Best Case
I wonder if it wouldn't be possible to just infer all of these types by just changing the stepslib
to track this dynamically growing object. ReturnType can be inferred from ProviderFn
and hopefully StepResolver
can return a compound type of whatever it got plus key+value from Provider
.
I tried around a bit but couldn't figure out how this works.
Alternative
If that's not possible I could imagine having the stepdefinition
file provide something like
interface SpecificContext extends StepContext {
a?: number
b?: boolean
c?: C
}
as a generic to processSteps
to at least use that for type validation.
Here's what I tried:
// stepslib-simplified.ts
import lodash_set from 'lodash/fp/set'
interface StepContext extends Object { }
type StepResolver = <Context extends StepContext>(context: Context) => Context // <- changed
// [...]
const Steps = {
// [...]
processSteps: <Context extends StepContext>(steps: StepResolver[]): void => { // <- changed
steps.reduce((prevContext, step) => step<Context>(prevContext), {})
// ^ Error: Argument of type '{}' is not assignable to parameter of type 'Context'.
// '{}' is assignable to the constraint of type 'Context', but 'Context' could be instantiated with a different subtype of constraint 'StepContext'.
}
}
Any ideas how to solve that?
NOTE: I admit step 4 using 'c.c2'
is more advanced. I'd be glad to at least get a solution for providing a direct keys without dot-nesting.
I found out there is a way to evaluate strings like <'${path}.${infer rest}'>
but don't yet know how that would be used there.