Combine two types into an interface elegantly in Typescript Combine two types into an interface elegantly in Typescript typescript typescript

Combine two types into an interface elegantly in Typescript


I think the following should work for you:

type BackendObject<  E extends Record<keyof E, keyof any>,  I extends Record<keyof E, any>  > = {    fields: {      [P in E[keyof E]]: I[{        [Q in keyof E]: E[Q] extends P ? Q : never      }[keyof E]]    }  }interface FooBackendObject extends  BackendObject<typeof FooNames, FooTypes> { }

The type BackendObject<E, I> is not an interface, but you can declare an interface for any particular concrete values of E and I as in FooBackendObject above. So, in BackendObject<E, I>, we expect E to be the mapping to keys (represented in FooBackendObject by the FooNames value, whose type is typeof FooNames... you can't just use the FooNames type here, since that doesn't contain the mapping.), and I to be the mapping to values (represented in the FooBackendObject by the interface FooTypes).

The mapped/conditional types being used might be a bit ugly, but this is what we're doing: first, the keys of the fields object come from the values of E (E[keyof E]). For each key P in that, we find the key of E which corresponds to it ({[Q in keyof E]: E[Q] extends P ? Q : never}[keyof E]), and then use that key to index into I for the value type.

Let's explain {[Q in keyof E]: E[Q] extends P ? Q : never}[keyof E] more fully. Generally a type like {[Q in keyof E]: SomeType<Q>}[keyof E] will be the union of SomeType<Q> for all Q in keyof E. You can cash it out with a concrete type if that makes more sense... if E is {a: string, b: number}, then {[Q in keyof E]: SomeType<Q>} will be {a: SomeType<'a'>, b: SomeType<'b'>}, and then we look up its values at keys keyof E, which is {a: SomeType<'a'>, b: SomeType<'b'>}['a'|'b'], which becomes SomeType<'a'> | SomeType<'b'>. In our case, SomeType<Q> is E[Q] extends P ? Q : never, which evaluates to Q if E[Q] matches P, and never otherwise. Thus we are getting the union of Q values in keyof E for which E[Q] matches P. There should be just one of those (if the enum doesn't have two keys with the same value).

It might be useful for you to go through the exercise of manually evaluating BackendObject<typeof FooNames, FooTypes> to see it happen.

You can verify that it behaves as desired. Hope that helps. Good luck!