Typed Generic Key Value Interface in Typescript

前端 未结 1 1936
深忆病人
深忆病人 2021-02-15 08:21

I have the following example Object:

let foo: Foo = {
  \'key1\': { default: \'foo\', fn: (val:string) => val },
  \'key2\': { default: 42, fn: (val:number) =         


        
1条回答
  •  一生所求
    2021-02-15 08:51

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

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

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

    function asFoo(foo: Foo): Foo {
      return foo;
    }
    

    This function works because the TypeScript compiler can do inference from mapped types, allowing it to infer T from Foo. 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; key2: FooValue;}
    

    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['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(foo: F & Foo): 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 string
    const key2fnOut = foo.key2.fn(123) // known to be number
    

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

    0 讨论(0)
提交回复
热议问题