Object with one key's value dependent on another key's value

Viewed 703

What would be the best way of achieving this effect (note: this is not valid syntax - consider it a pseudocode):

type Config = {
  [key: string]: <T> {
    params: T,
    doSth: (params: T) => void,
  },
}

I.e. the generic T type is different for every key's value (which is an object itself), but at the same time it's being reused within that value (object) under different fields. Also, T is derived from the params field. With that I could do:

const config: Config = {
  a: {
    params: { x: 123 },
    doSth: params => {}, // Type of "params" is "{ x: 123 }"
  },
  b: {
    params: { y: 'asd' },
    doSth: params => {}, // Type of "params" is "{ y: 'asd' }"
  },
}

To my surprise, googling this doesn't yield matching results, even though it seems relatively useful pattern - there's a lot of similar problems but not really the same. Some solutions to those, that I tried to apply - all failed.

1 Answers

There are some things you can't do in TypeScript with types alone, sometimes you need a function to help even if the function doesn't actually do anything at runtime. I think this is one of those times. Here's an example of a function that gets close to what you're describing:

type ConfigEntry<T extends object> = {
    params: T;
    doSth: (params: T) => void;
};

function makeConfigEntry<T extends object>(obj: ConfigEntry<T>): ConfigEntry<T> {
    return obj;
}

Notice how ConfigEntry lets us relate the two places we'll be using that type. You'd use that function when defining the values of the properties:

const config = {
    a: makeConfigEntry({
        params: { x: 123 },
        doSth: params => {}, // Type of `params` is `{ x: number; }`
        //     ^?
    }),
    b: makeConfigEntry({
        params: { y: "asd" },
        doSth: params => {}, // Type of `params` is `{ y: string; }`
        //     ^?
    }),
};

Note there's no Config type annotation on that. TypeScript will infer it from the object initializer.

But notice the types on params aren't quite what you wanted. TypeScript won't infer the types { x: 123; } and { y: "asd"; } from your objects. It will either infer { x: number; } and { y: string; }, or if you define the objects as const, it'll infer { readonly x: 123; } and { readonly y: "asd"; }:

const config = {
    a: makeConfigEntry({
        params: { x: 123 } as const,
        doSth: params => {}, // Type of `params` is `{ readonly x: 123; }`
        //     ^?
    }),
    b: makeConfigEntry({
        params: { y: "asd" } as const,
        doSth: params => {}, // Type of `params` is `{ readonly y: "asd"; }`
        //     ^?
    }),
};

This is likely not quite what you wanted, but I think it's close, probably one of several approaches that are as close to what you want as you can get with today's TypeScript.

Related