How to generically type a modified dictionary of inconsistent functions

Viewed 84

I have a function that needs to convert a dictionary of functions with partially dynamic properties into a new dictionary of functions with similar properties.

What I have so far:

interface Dictionary<T extends any> {
    [key: string]: T
}

type InputFunction<T> = (value: T, parameters?: any) => T

type InputFunctionMap<T> = Dictionary<InputFunction<T>>

type OutputFunction = (parameters?: any) => void

type OutputFunctionMap<T, U extends InputFunctionMap<T>> = {
    [P in keyof U]: OutputFunction
}

function create<T>(key: string, value: T): [() => T, (value: T) => void]
function create<T, U = InputFunctionMap<T>>(key: string, value: T, inputs: U): [() => T, Readonly<OutputFunctionMap<T, U>>]
function create<T, U = InputFunctionMap<T>>(key: string, value: T, inputs?: U): [() => T, (value: T) => void] | [() => T, Readonly<OutputFunctionMap<T, U>>] {
    // ...
}

What I'm trying to accomplish:

const [getFoo, updateFoo] = create('foo', 'FOO')

getFoo() // => 'FOO'

updateFoo('foo') // => 'foo'

getFoo() // => 'foo'

interface MathParameters {
    operation: 'MULTIPLY' | 'DIVIDE'
    operand: number
}

const [getBar, {addToBar, addSevenToBar, applyMathToBar}] = create('bar', 0, {
    addToBar: (value, parameters: number) => value + parameters,
    addSevenToBar: (value) => value + 7,
    applyMathToBar: (value, parameters: MathParameters) => {
        switch (parameters.operation) {
            case 'MULTIPLY':
                return value * parameters.operand

            default:
                return value
        }
    }
})

getBar() // => 0

addSevenToBar()

getBar() // => 7

addToBar(3)

getBar() // => 10

applyMathToBar({operation: 'MULTIPLY', operand: 2})

getBar() // => 20

The problem is that I can't figure out how to properly and generically type parameters (I don't want them to be any) so that the type can be inferred from the input function and be correctly available as the parameter in the output function.

Update:

Thank you for the support so far!

I'm just having trouble getting the ouput mapping to work as intended: Typescript Playground

type TailParams<T> = T extends (arg: any, ...args: infer A) => void
    ? A
    : never

const input = inputs[name as keyof U]

outputs[name as keyof U] = (...args: TailParams<typeof input>) => {
    //
}

Throws: Type '(...args: TailParams<U[keyof U]>) => void' is not assignable to type 'TailFunc<U[keyof U]>'.

1 Answers

I'd probably type create() like this:

type TailFunc<T> = T extends (h: any, ...args: infer A) => infer R ? (...args: A) => R : never;

declare function create<T>(key: string, value: T): [() => T, (value: T) => void]
declare function create<T, U extends Record<keyof U, (value: T, ...args: any) => any>>(
    key: string,
    value: T,
    inputs: U
): [() => T, { [K in keyof U]: TailFunc<U[K]> }]

The TailFunc<T> type takes a function type T and returns a new function where the first parameter has been removed. So TailFunc<(a: string, b: number, c: boolean) => string> becomes (b: number, c: boolean) => string.

The second call signature for create() is generic in T as usual, and U, which is the type of the inputs parameter. This is constrained to Record<keyof U, (value: T, ...args: any)=>any, so it expects U to be an object type whose values are all functions whose first parameter is of type T.

The return type of that call signature is a tuple where the second element is the mapped type that performs TailFunc on all the properties of U.

Let's make sure it works:

getBar() 
addSevenToBar()
getBar() 
addToBar(3)
getBar() 
applyMathToBar({ operation: 'MULTIPLY', operand: 2 })
getBar() 

Those all give no errors in the type system, so the types are what you want. Whether or not it behaves as you expect at runtime depends on the implementation of create(), I guess.

Okay, hope that helps; good luck!

Link to code


note that how to implement create() so that everything typechecks with the minimum of type assertions is outside the scope of the original question, so I'm only posting a link to one possible way to do it: link to code

Related