Typescript error when an object literal has functions

Here's the code:

function id<T>(x: T) {
    return x
}

const keys: string[] = []

const obj1 = Object.fromEntries(keys.map(k => [
    k, 
    {test() {}} // OK
]))

const obj2 = Object.fromEntries(keys.map(k => [
    k, 
    id({test() {}}) // Error
]))

const obj3 = Object.fromEntries(keys.map(k => [
    k, 
    id({test: new Date()}) // OK
]))

I have no idea why typescript gives this error:

Argument of type '{ test(): void; }' is not assignable to parameter of type 'T'.
  'T' could be instantiated with an arbitrary type which could be unrelated to '{ test(): void; }'.

It seems that only functions in object literal cause this error.

The id function here is just for demo. Actual function has this signature:

<T>(x: T) => T & SomeExtraFields

Typescript version: 4.2.3

2 answers

  • answered 2021-05-03 18:20 Syed Mohib Uddin

    Try it now.

    function id<T>(x: T): T {
       return x
    }
    
    const keys: string[] = []
    
    const obj1 = Object.entries(keys.map(k => [
        k,
        { test() { } } // OK
    ]))
    
    const obj2 = Object.entries(keys.map(k => [
        k,
        id({ test() { } }) // Ok
    ]))
    
    const obj3 = Object.entries(keys.map(k => [
        k,
        id({ test: new Date() }) // OK
    ]))
    

  • answered 2021-05-03 21:00 captain-yossarian

    YOu have sevaral ways to handle it:

    First:

    If you use reference instead of literal type, it works as expected:

    const id = <T,>(x: T): T => x
    
    const keys: string[] = []
    
    const iter = keys.map(k => [
        k,
        id({ test() { } }) // ok
    ])
    
    const obj2 = Object.fromEntries(iter)
    
    

    OR:

    const idResult = id({ test() { } })
    const obj2 = Object.fromEntries(keys.map(k => [
        k,
        idResult
    ]))
    
    

    Second

    Let's try to use arrow function as property instead of method:

    const obj2 = Object.fromEntries(keys.map(k => [
        k,
        id({ test: () => { } }) // ok
    ]))
    
    

    Strange behavior, is not it?

    Let's take a look on Object.fromEntries signature:

    fromEntries<T = any>(entries: Iterable<readonly [PropertyKey, T]>): { [k: string]: T };
    

    So, TS tries to infer T generic type. In our case it is ReturnType of id function.

    But for some reason, TS is unable to do it.

    Third:

    In order to help TS to infer the type, you can provide explicit generic argument for Array.prototype.map

    const obj2 = Object.fromEntries(
      keys.map<[string, { test(): void }]>((k) => [k, id({ test() {} })])
    );
    

    Fourth

    You can also handle it without any explicit generic arguments.

    You can just add constraint to T generic in your id function definition.

    interface Method {
      (): void;
    }
    const id = <T extends { [prop: string]: Method }>(x: T) => x;
    
    const keys: string[] = [];
    
    const obj2 = Object.fromEntries(keys.map((k) => [k, id({ test() {} })])); // ok
    
    
    /////////////////////////
    
    type ArrowProp = () => any;
    const id = <T extends { [prop: string]: ArrowProp }>(x: T) => x;
    
    const keys: string[] = [];
    
    const obj2 = Object.fromEntries(keys.map((k) => [k, id({ test() {} })]));
    
    

    Fifth

    You can even use second generic to help TS to infer the type

    const id = <Prop, T extends Record<string, Prop>>(x: T) => x;
    // OR
    const id = <T extends object>(x: T) => x;
    
    
    const keys: string[] = [];
    
    const obj2 = Object.fromEntries(keys.map((k) => [k, id({ test() {} })]));
    

    Unfortunately, I'm unable to explain why TS can't infer method property without any workarounds