Home > Blockchain >  TypeScript: Apply generic function to all properties of object and infer return value
TypeScript: Apply generic function to all properties of object and infer return value

Time:09-09

I try to solve the following problem in TypeScript:

type State = {
    alpha: string,
    beta: number
}

const selectors = {
    alpha: (s: State) => s.alpha,
    beta: (s: State) => s.beta
}

function createModifiedSelector<T, S, U>(
  modifyState: (differentState: T) => S,
  selector: (state: S) => U,
) {
  return (differentState: T) => selector(modifyState(differentState));
}

const add2State = (state: any) => {
    return {...state, alpha: "abc", beta: 123}
}

// the following works fine
const modifiedAlpha = createModifiedSelector(add2State, selectors.alpha)
console.log(modifiedAlpha({}));

// but what if I would like to convert the whole selectors object (if they are many)?
function createModifiedSelectorsObj<T, S, R extends typeof selectors>(modifyState: (differentState: T) => S, selectors: R) {
    const result: Record<keyof R, any> = selectors;     // which type to use here instead of any?
    let selectorKey: keyof R;
    for (selectorKey in selectors) {
        result[selectorKey] = createModifiedSelector(modifyState, selectors[selectorKey])       // how to solve TS2345 here?
    }
    return result;
}

Link to TS Playground.

Is there a way in TS to provide correct typings for createModifiedSelectorsObj and the return values of the selector functions?

CodePudding user response:

Here's one way to do it:

function createModifiedSelectorsObj<T, S, U extends object>(
    modifyState: (differentState: T) => S, selectors: { [K in keyof U]: (s: S) => U[K] }) {
    const result = {} as { [K in keyof U]: (t: T) => U[K] };
    let selectorKey: keyof U;
    (Object.keys(selectors) as Array<keyof U>).forEach(<K extends keyof U>(selectorKey: K) =>
        result[selectorKey] = createModifiedSelector(modifyState, selectors[selectorKey])
    );
    return result;
}

The T and S type parameters are the same as for a single selector, but now U is an object type whose keys are the same as the selectors object and whose values are the return types of the methods in the selectorsobject. So if selectors is of type {a: (s: State)=>string, b: (s: State)=>number}, then U will be just {a: string, b: number}.

That means the type of selectors is a mapped type over U, and when you call createModifiedSelectorsObj() the compiler needs to infer U from {[K in keyof U]: (u: S)=>U[K]}. Luckily this sort of inference is pretty easy for the compiler.

Inside the implementation, I define result's type as a different mapped type over U, where this time the input for each method is of type T instead of S. Note that I have changed the implementation to initialize result to a new empty object {} instead of selectors, since I can't imagine why you'd want to clobber the properties of the initial object. Feel free to change it back if you want, but I hope you know what you're doing.

Anyway, since the initialized value ({}) is not really of the type we want result to be, I need to use a type assertion to get the compiler to accept the assignment.

Then I've refactored the for...in loop to an Object.keys().forEach(), because it allows me to use a callback where selectorKey is of generic type K extends keyof U. Note that Object.keys(obj) returns string[] and not (keyof typeof obj)[], so I have used a type assertion to tell the compiler that it can assume that the only keys in selectors are the ones it knows about, which are of type keyof U.

The implementation of the generic callback type checks because the compiler understands that both sides of the assignment are of type (t: T) => U[K].


Okay, let's test it out:

const modifiedSelectorsObj = createModifiedSelectorsObj(add2State, selectors);
/* const modifiedSelectorsObj: {
    alpha: (t: any) => string;
    beta: (t: any) => number;
} */

Looks good. The type of modifiedSelectorsObject is an object type whose methods have the same return type as the corresponding methods of selectors, but which have the same input type as add2State.

Playground link to code

  • Related