When used with TypeScript, Object.values() fails to derive the correct type from a union of Records:
type A = Record<string, number>;
type B = Record<string, boolean>;
function func(value: A | B) {
const properties = Object.values(value); // any[]
}
TS Playground
I expect properties to be of type number[] | boolean[], but the actual type is any[].
Is there a way to derive the correct type from Object.values()?
When you call Object.values(v) the compiler tries to match v with {[k: string]: T} for a generic type argument T that it infers (according to this call signature).
So if v is of a type like {[k: string]: Z}, then the compiler will infer T as Z, and everything will succeed. But if v's type is a union of records like {[k: string]: X} | {[k: string]: Y}, things are different. One might hope that T could be inferred as the union X | Y. But the inference algorithm does not work that way. The type X | Y does not appear anywhere directly in the input type, and the compiler will not synthesize union types from multiple inference candidates. Instead the compiler picks one candidate ... let's say, X ... and then issues an error if Record<string, X> | Record<string, Y> is not assignable to Record<string, X>.
This refusal to synthesize union types is intentional; often people would rather see an error than have the compiler start creating unions to make things succeed, especially because in many cases that would make just about every call succeed, even ones doing crazy things like compare(3, "oops"), assuming compare is of a type like <T>(a: T, b: T) => number. People want to see that fail instead of having T be inferred as number | string.
In your case though you want that union to be inferred. There is an open feature request at microsoft/TypeScript#44312 to have a way to say that such an inference should happen. But it's not part of the language yet, and even if it were, the Object.values() call signature might not be changed to use it.
So we can't rely on the compiler to infer the union type you want directly.
Instead, we can specify the union type when we call Object.values(). That would look like Object.values<X | Y>(v). In your example, then you could just write Object.values<number | boolean>(value), and you're done.
But you don't want to hard code that type argument that way. You'd like it to depend on value such that if its type changes to some other union of records, things would continue to work. So that means: given a value value of a type like {[k: string]: X} | {[k: string]: Y}, how can we compute the type X | Y?
Here we can use the typeof type query operator on value to get the type of value, and then we can index into that type with string to get the type of the properties of value at a string key.
That is:
Object.values<typeof value[string]>(value); // okay
Or, to see the general example:
function foo<X, Y>() {
type A = Record<string, X>;
type B = Record<string, Y>;
function func(value: A | B) {
return Object.values<typeof value[string]>(value);
}
// func(value: Record<string, X> | Record<string, Y>): (X | Y)[]
}
Inside foo(), the func() function accepts a value of type A | B, and returns a value of type (X | Y)[], without us having to hardcode the type X | Y ourselves. Instead, typeof value[string] evaluates to X | Y and we use that.
Playground link to code
This is how you can define the interface for a patched object construct named ObjectConstructorAlt.
declare interface ObjectConstructorAlt {
values<T>(o: T):
T extends any ?
T extends Record<string|number, infer V> ? V[]
: T extends (infer V)[] ? V[]
: any
: never
;
}
type A = Record<string, number>;
type B = Record<string, boolean>;
declare const a:A;
declare const b:B;
declare const ab:A|B;
Object.values(a); // number[]
Object.values(b); // boolean[]
Object.values(ab); // any[]
// actual implementation
const ObjectAlt: ObjectConstructorAlt = {
values<T>(x: T){
return Object.values(x as object) as any;
}
}
ObjectAlt.values(a); // number[]
ObjectAlt.values(b); // boolean[]
ObjectAlt.values(ab); // number[] | boolean[]
Typescript
If you could persuade the TypeScript maintainers to swap the current ObjectConstructor
interface ObjectConstructor {
values<T>(o: { [s: string]: T } | ArrayLike<T>): T[];
values(o: {}): any[];
...
}
for something like ObjectConstructorAlt then a patch on your end wouldn't be necessary. That's unlikely though, as it could increase compile time and result in other things breaking.
update - FWIW I submitted this as an issue to TypeScript and the present behavior of Object.values/entries is determined to be not a defect. There is a lot of history behind that decision which is worth reading.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With