Typed Generic Key Value Interface in Typescript Typed Generic Key Value Interface in Typescript typescript typescript

Typed Generic Key Value Interface in Typescript


How about defining Foo<T> to be a mapped type, like this:

interface FooValue<T> {  default: T;  fn: (val: T) => any;}type Foo<T> = {  [K in keyof T]: FooValue<T[K]>}

In this case, if T is some normal object type like {a: string, b: number, c: boolean}, then Foo<T> is the Foo-ized version of it: {a: FooValue<string>, b: FooValue<number>, c: FooValue<boolean>}. Now you can make a helper function which accepts an object literal only if it can be inferred as a Foo<T> for some type T:

function asFoo<T>(foo: Foo<T>): Foo<T> {  return foo;}

This function works because the TypeScript compiler can do inference from mapped types, allowing it to infer T from Foo<T>. Here is it working:

let foo = asFoo({  key1: { default: 'foo', fn: (val: string) => val },  key2: { default: 42, fn: (val: number) => val }});// inferred as { key1: FooValue<string>; key2: FooValue<number>;}

And here is it failing:

let badFoo = asFoo(  key1: { default: 'foo', fn: (val: string) => val },  key2: { default: 42, fn: (val: number) => val },  key3: { default: true, fn: (val: string) => val }}); // error! Types of property 'key3' are incompatible. // Type 'boolean' is not assignable to type 'string'.

Hope that helps. Good luck!


Update: The above code assumes you're okay with foo.key1.fn('abc') being inferred as type any, since FooValue<string>['fn'] is defined as a function that returns any. It kind of forgets the output type from the original object literal. If you want foo to remember the return type of its properties' fn methods, you can do this slightly different helper function:

function asFoo<T, F>(foo: F & Foo<T>): F {  return foo;}let foo = asFoo({  key1: { default: 'foo', fn: (val: string) => val },  key2: { default: 42, fn: (val: number) => val },  // next line would cause error  // key3: { default: true, fn: (val: string)=>val} })const key1fnOut = foo.key1.fn('s') // known to be stringconst key2fnOut = foo.key2.fn(123) // known to be number

And that works. In this case, asFoo() just verifies that the input is a Foo<T> for some T, but it doesn't coerce the output type to a Foo<T>. Depending on your use cases, you may prefer this solution to the other one. Good luck again.