Search code examples
typescripttypescript-typingstypescript-generics

How to determine the type of a property that depends on the type of another property of the same object


See example. I have type TConfig. I want to get suggestions of available fields in function parameter. This is example for working with redux store(reducer, state).

type TParam = any;

type TFunction = (param: TParam) => TParam;

type TConfig = Record<string, {func: TFunction, param: TParam}>;

function handleConfig(config: TConfig) {};


type TMyParam1 = {
  prop1: string;
  prop2: number;
}

type TMyParam2 = {
  prop3: boolean;
  prop4: string;
}

handleConfig({
  '1': {
    func: (param: TMyParam1) => param,
    param: {
      // I want suggestions of available fields in TMyParam1
    }
  },
  '2': {
    func: (param: TMyParam2) => param,
    param: {
      // I want suggestions of available fields in TMyParam2
    }
  }
})

Solution

  • In order for this to work, you need handleConfig() to be generic. Conceptually, for each property key of the config input ("1" and "2" in your example), you want a single property value type that serves as both the type of the param property, and the input and output type of the func property (TMyParam1 and TMyParam2 in your example). So let's make it generic in terms of an object type T corresponding to that shape ({"1": TMyParam1; "2": TMyParam2} in your example):

    function handleConfig<T extends object>(config: { [K in keyof T]: {
        func: (param: T[K]) => T[K],
        param: T[K]
    } }) { };
    

    So config's type is a mapped type over the properties of T.

    This basic approach should work (you will get errors when and only when you make a mistake), but it seems you want to be able to get IntelliSense hints when func is specified but param is not. And that means you want T to be inferred only from func, and then param is just checked against it, not inferred from it. So you want to block inference on param, as described in microsoft/TypeScript#14829. The next release of TypeScript should include a native NoInfer<T> utility type, as implemented at microsoft/TypeScript#56794, at which point you can just write

    function handleConfig<T extends object>(config: { [K in keyof T]: {
        func: (param: T[K]) => T[K],
        param: NoInfer<T[K]>
    } }) { };
    

    and things will behave as you expect:

    handleConfig({
        '1': {
            func: (param: TMyParam1) => param,
            param: {
                // suggests prop1 and prop2
            }
        },
        '2': {
            func: (param: TMyParam2) => param,
            param: {
                // suggests prop3 and prop4
            }
        }
    })
    

    Until that release happens you can define your own NoInfer utility in a number of ways; here's one that works for this example:

    type NoInfer<T> = T extends infer U ? U : never
    

    (You can look at microsoft/TypeScript#14829 for this and other workarounds and explanations for how they work).

    Playground link to code