Eric Simonton
Eric Simonton

Reputation: 6039

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>;

Upvotes: 0

Views: 101

Answers (2)

jcalz
jcalz

Reputation: 329443

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

Upvotes: 2

kaya3
kaya3

Reputation: 51102

You don't need to use Record for this; keyof is enough.

function keyableValues<T>(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 };

let result = keyableValues(asInterface);

Then the type of result is inferred as (string | number)[].

Upvotes: 0

Related Questions