Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Object.values doesn't infer type when extending Record type #30805

Closed
Krumpet opened this issue Apr 8, 2019 · 6 comments
Closed

Object.values doesn't infer type when extending Record type #30805

Krumpet opened this issue Apr 8, 2019 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Krumpet
Copy link

Krumpet commented Apr 8, 2019

TypeScript Version: 3.5.0-dev.20190407

Search Terms:
record, Object.values

Code

interface onlyStrings extends Record<keyof onlyStrings, string> {
	a: string;
	b: string;
}

const b: onlyStrings = null;
Object.values(b); // any[]
Object.values<string>(b); // error, index signature is missing in onlyStrings

Expected behavior:
Object.values(object) for an object whose type extends Record<K,V> should return array with type V[]

Actual behavior:
Object.values(b) as above returns any[], trying to specify values<string> gives an error that onlyStrings is missing index signature. Because it is extending a Record it should know the type for the returned values even without an index signature.

It does work when working with the Record type directly instead of extending, or when extending and including an index signature, or when the key type is explicit:

const a: Record<keyof onlyStrings, string> = null;
Object.values(a); // string[]

interface stringsWithIndexSignature extends onlyStrings {
	[s: string]: string;
}
const c: stringsWithIndexSignature = null;
Object.values(c); // string[]

interface stringMap extends Record<string, string> {
    a: string;
    b: string;
}
const d: stringMap = null;
Object.values(d) // string[]

It can be made to work by wrapping in a function and specifying the generic types:

function typedValues<U, T extends Record<keyof T, U>>(recordObject: T): U[] {
	return Object.values(recordObject);
}

typedValues(b); // {}[]
typedValues<string, onlyStrings>(b); // string[]

Playground Link: Object.values is not supported in TS Playground. I tried to reproduce with Object.keys().map() but that returns any[] regardless of original type.

Related Issues: possibly #26010 - Object.values and Object.entries return type any when passing an object defined as having number keys and #21089 - [TS2.5 regression] Object.values(someEnum) returns string[] (not any[]/number[])

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Apr 9, 2019
@RyanCavanaugh
Copy link
Member

Strongly typing Object.values has the same issue as strongly typing Object.keys - subtyping Record<K, V> does not imply that all Object.values can't return a non-V element.

@Krumpet
Copy link
Author

Krumpet commented Apr 9, 2019

What about my last example, where subtyping does give the right type? Wouldn't it be wrong to return V[] in this case?

interface stringMap extends Record<string, string> {
    a: string;
    b: string;
}
const d: stringMap = null;
Object.values(d) // string[]

Another question - how can Object.values return a non-V element? I've tried subtyping and adding a non-K attribute or assigning an object with the wrong type and get errors in those cases.

Thanks for the feedback!

@RyanCavanaugh
Copy link
Member

Record<string, T> is sort of a different beast than Record<"A" | "B", T> - the former implies a stronger guarantee about what values might be observed.

Another question - how can Object.values return a non-V element?

type Point2D = Record<"x" | "y", number>;
function fn(obj: Point2D) {
  console.log(Object.values(obj).some(v => typeof v !== "number"));
}

const NamedPoint = { x: 0, y: 0, name: "origin" };
fn(NamedPoint);

@typescript-bot
Copy link
Collaborator

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

@tal
Copy link

tal commented Jun 11, 2019

I understand the concern, but it would be nice to be able to provide some ability to have types that can convey the completeness of the object for these iterators. Iran into an issue like this:

type SetData = { foo: string }

type UnionSet<Keys extends string> = { [K in Keys]: SetData }

function handleGenericSet<T extends string>(set: UnionSet<T>) {
  const entries = Object.entries(set) // const entries: [string, unknown][] expect [string, SetData][]
  const values = Object.values(set) // const values: unknown[] expect SetData[]
  const keys = Object.keys(set) // const keys: string[] expect T
}

type MyUnion = 'a' | 'b' | 'c'

function handleSpecificSet(set: UnionSet<MyUnion>) {
  const entries = Object.entries(set) // const entries: [string, SetData][] expect [string, SetData][]
  const values = Object.values(set) // const values: unknown[] expect SetData[]
  const keys = Object.keys(set) // const keys: string[] expect MyUnion
}

@olejorgenb
Copy link

While the trick of inheriting from Record<string, T> for uniformly valued types works, it would be nice if an special case could be made for frozen literals.

interface Interface {
    a: string
    b: string
}

const x: Interface = Object.freeze({a: "a", b: "b"})
// or nicer but maybe confusing? Kinda like readonly arrays/tuples:
// const x: readonly Interface = {a: "a", b: "b"}

const y = Object.values(x)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants