typescript

Interface treated differently than equivalent object


In the snippet below, the two seemingly equivalent lines at the end apparently are not. I want to write my function definition such a way that neither gives an error, and both return (string | number)[]. Any ideas?

function keyableValues<T extends Record<any, keyof any>>(o: T): T[keyof T][] {
    const values: T[keyof T][] = [];
    for (const key of Object.getOwnPropertyNames(o)) {
        values.push(o[key as keyof T]);
    }
    return values;
};

interface O {
    a: string;
    b: number;
}
let asInterface: O = { a: 'hi', b: 2 };
const notAsInterface = { a: 'hi', b: 2 };

keyableValues(asInterface);    // <- typing error, returns (string | number | symbol)[]
keyableValues(notAsInterface); // <- no error, returns (string | number)[]

The error on the second-to-last line is:

Argument of type 'O' is not assignable to parameter of type 'Record'.

Index signature is missing in type 'O'.(2345)

Here it is in the typescript playground.

Edit Please note this is a simplified example. It is important to maintain the restriction on values to be assignable to keyof any. My real use case is a function that maps the values in a collection to the keys of a new object:

function mapAsKeys<T extends Record<any, keyof any>, V>(
  object: T,
  iteratee: ObjectIteratee<T, V>,
): Record<T[keyof T], V>;

Solution

  • For better or worse, interface types don't get implicit index signatures, whereas type aliases do. See the relevant GitHub issue microsoft/TypeScript#15300 for more information.

    My suggestion here is to use a self-referential generic constraint instead, where T extends Record<keyof T, ...>, which should be true for any object type whether it's an interface or a type alias. Like this:

    function keyableValues<T extends Record<keyof T, keyof any>>(o: T): T[keyof T][] {
        const values: T[keyof T][] = [];
        for (const key of Object.getOwnPropertyNames(o)) {
            values.push(o[key as keyof T]);
        }
        return values;
    };
    

    That should fix the issue:

    keyableValues(asInterface);    // <- no error, returns (string | number)[]
    keyableValues(notAsInterface); // <- no error, returns (string | number)[]
    

    Link to code