Issue having ReturnType inferred in a config parameter of a function

Viewed 98

I've simplified my use case to this playground:

Playground

And here is the same code:

function createEntityComponentSystem<
    Config extends {
        [SystemName in SystemNames]: {
            createComponent: () => Record<string, any>;
            run: (
                component: ReturnType<Config[SystemName]['createComponent']>,
            ) => void;
        }
    },
    SystemNames extends (keyof Config) & string,
>(config: Config) {
    // Create Entity Component System
}


createEntityComponentSystem({
    systemA: {
        createComponent: () => ({ x: 0, y: 0 }),
        run: (
            // Why isn't this inferred as { x: number; y: number } ?
            // In fact, if I change this to:
            // component: { x: string; y: string; }
            // the compiler throws an error, because it KNOWS
            // that component should be { x: number; y: number; }
            // so why can't it infer it?
            component,
            // Uncomment to try:
            // component: { x: string; y: string; },
        ) => {
        },
    }
});

If you're wondering why this exists in a record of SystemName of SystemNames, it's because it's for an Entity Component System. The systems are provided as a single config object, because I'm able to actual validate many type constraints between them when done this way. This is the single outlying issue I'm encountering, as inference has otherwise worked great.

1 Answers

I unfortunately cannot give details as to why your solution does not work--although the main issue seems to be referring to Config inside of Config. Either way, here's how you can make it infer...


Let's assume we had the type

type Config = {
  systemA: {
      x: number;
      y: number;
  };
}

We can write a type Converter<T> such that Convert<Config> outputs the type you want for the function (object with createComponent and run typings):

type Converter<Cfg> = {
  [SystemName in keyof Cfg]: Component<Cfg[SystemName]>
}

interface Component<R> {
  createComponent: () => R;
  run: (component: R) => void;
}

If we run Converter<Config>, We're taking each key of our Config type and doing something like:

Component<Config['systemA']>
// Which is the same as
Component<{ x: number; y: number }>
// which is the same as
{
  createComponent: () => { x: number; y: number; };
  run: (component: { x: number; y: number; }) => void;
}

Now, we don't want to have to manually define this Config type above, but that's ok! We can actually have TypeScript infer this type instead:

function createEntityComponentSystem<Config>(
  config: Converter<Config>
) {}

And this will work! In your example in the original problem, it will infer Config to be:

{
  systemA: {
      x: number;
      y: number;
  };
}

and, therefore, be able to correctly identify the type of the component parameter.


The trick here is not having TypeScript directly infer what you want, but infer an intermediate type, which you then convert to your more complex type.

Related