TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

Unnecessary any returned from Object.values and Object.entries

Open typeholes opened this issue 1 year ago • 10 comments

⚙ Compilation target

es6

⚙ Library

es2017.object

Missing / Incorrect Definition

This is a duplicate of #26010 which I believe can now be fixed. Object.entries and Object.values can now return the correct types without impacting enums.

https://www.typescriptlang.org/play/?target=99&jsx=0&exactOptionalPropertyTypes=true&lib=lib.esnext.d.ts&ts=5.5.0-dev.20240429#code/PTAEAEBcEMCcHMCmkBcpEGcB2iAekAoECAGwEsAjNTHfAgxLAVwFtQANUAbwEEAaUACEAvvQDGAeywZIoCSQAm7DKAC8oAPIUAVojGQAdADdoJJpgAU7AJQBuImFBOAegH4Ck6bNwr1Js5Y29sROoG70ZFiQiLAAZtBiiKAAktwETgDauGjMLBQxALpoMrCR8KAAPqDMJCT2oh5SMnJoqepcoACsaADk0TI9AgAcOUy1oML2ns3yCgBqapo6eob+5hgWEnYOoWHu07JGi2uWW8GOLu70xFgS0aCQABaYSZJjCqCmGBKg+aAYAAdyIcyNBPqAmFgyFJQJEZIhoB8JLE5EYYiQJIiVBQmLJYhJYKAAMSdIYAdgAzAAmAixSH6aFYJyMSClTAAHgAKgA+TZoDoZDDFVllIqgTm2CagaxoQUirDwAScgoZAqgWn0yCM5lRNkYLm8iT80CC0Z5QpoCVSmUmkplJUqtUarAMmHoXVkDk8vncW3C0oKiq5fKwMVW4TS2V2hUO1W+2DIJiwJlxhp0l1amEnfXeo2+03-eXwMOSiM2zmq52upnZg0+gVC6qsEMl62Wyvp6ugWu540F6PwIPNi3i0uR8VxjoJyBJlNq0RAA

Sample Code

interface I {
  [x: number]: string | null;
}

const o: I = { 5: 'test', 8: null };
Object.values(o); // currently `any[]`  should be `(string|null)[]`

Documentation Link

No response

typeholes avatar Apr 30 '24 17:04 typeholes

This is working as intended.

This is perfectly legal code, and a return type of (string | null)[] would be wrong:

const o = { foo: 123, 5: 'test', 8: null };
const i: I = o;

MartinJohns avatar Apr 30 '24 17:04 MartinJohns

See this FAQ entry: https://github.com/microsoft/TypeScript/wiki/FAQ#indirect-excess-properties-are-ok

nmain avatar Apr 30 '24 17:04 nmain

Scratch that, I see the issue and it should really be returning unknown[]

I understand how that FAQ applies to Object.keys, but I do not think it applies here.

const o = { foo: 123, 5: 'test', 8: null };
const i: I = o;

Here you have explicitly type i: I so I would expect that Object.values would return I[string|number][] (edit: per fatCerebus's correction)

note that if you let i be inferred the result would be (string | number | null)[] which is also correct

typeholes avatar Apr 30 '24 18:04 typeholes

Why would Object.values(i) return I[]? None of the properties are Is. The point Martin is making is that TS is only seeing the types here and there's no guarantee that a thing typed as I only has string | null values.

fatcerberus avatar Apr 30 '24 19:04 fatcerberus

Scratch that, I see the issue and it should really be returning unknown[]

Agreed, but that would be a very big breaking change for little gain.

Here you have explicitly type i:

That was intentional to demonstrate the point. You don't know where and how the object was created.

MartinJohns avatar Apr 30 '24 19:04 MartinJohns

@typeholes Just to be clear: From the type checker's point of view there's no difference between the explicitly typed o in your example and the explicitly typed i in Martin's. Returning (string | null)[] when Object.values is called on an I wouldn't be sound.

fatcerberus avatar Apr 30 '24 19:04 fatcerberus

Interestingly we have the exact same issue with a record, but do get the specific result type


type T = Record<number, string|null>
interface I { [x: number]: string | null ; } 

const o = { foo: 123, 5: 'test', 8: null };
const oT: T = o; const ovT = Object.values(oT);  // (string | null)[]
const oI: I = o; const ovI = Object.values(oI);  // any[]

https://www.typescriptlang.org/play/?target=99&jsx=0&exactOptionalPropertyTypes=true&lib=lib.esnext.d.ts&ts=5.5.0-dev.20240429#code/PTAEAEBcEMCcHMCmkBcpEGcB2iAekAoECAGwEsAjNTHfAgsrSRWAM2gGNFQBJUAb1ABtXGiwBXALYUWAXTQZIsRvFAAfUBJIlQAblABfUPUgBPAA7cAKqAC8oAEqIOAe1gATADwTpLADSgispY8GpaJAB89K5YiqAudgKgrC4uaACMAEwAzAEArGgA5MyKhQEAHGLi2oa6BDFxLlZoNvYu+g2Q8QBuraAA8hQAVs6QAHTd0CTimAAUTQCU+qDEs0Eq6prVJAtCskRgoEfHJ6enAHoA-PUusV0uPGh8bR23jd3PA8OjE1MzGPMeEsjsRoFhTHsDmdoTCrvQCEA

typeholes avatar Apr 30 '24 19:04 typeholes

Object.values has an overload for array-likes and string index signatures (but not bare numeric index signatures).

    values<T>(o: { [s: string]: T; } | ArrayLike<T>): T[];

Supporting arrays makes sense. I'm guessing the string index signature is meant for object-as-a-dictionary scenarios. Either one has a potential hazard with excess properties, though.

snarbies avatar Apr 30 '24 20:04 snarbies

Correct, so my question is should we be inconsistent between the interface and record values. I think consistency would be better, especially when you consider that any is just explicitly unsound.

typeholes avatar Apr 30 '24 20:04 typeholes

It's not really inconsistent though. Index signatures and interfaces represent different things.

Index signatures are "arrayish." They may not necessarily have numeric keys, but they represent collections of key/value pairs where the "element" values all conform to a single type, so it makes sense for Object.values to support this scenario.

By its very nature, an interface is not exhaustive. An interface represents a subset of an object's actual shape (or a commonality among otherwise differently-shaped objects). So even if Object.values returned a union of known property types based on the interface, you would still have to include unknown in the union to account for properties not identified by the interface. And if you include unknown in the union, then the entire thing reduces to unknown.

That it returns any instead of unknown is unfortunate, but it's due to historical reasons.

snarbies avatar May 02 '24 12:05 snarbies

Implementation is in #58358

typeholes avatar May 03 '24 15:05 typeholes

This issue has been marked as "Question" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

typescript-bot avatar May 06 '24 01:05 typescript-bot