The Wayback Machine - https://web.archive.org/web/20210805112143/https://github.com/microsoft/TypeScript/issues/35745
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

Support known possible keys in Object.entries and Object.fromEntries #35745

Open
4 of 5 tasks
wucdbm opened this issue Dec 18, 2019 · 9 comments
Open
4 of 5 tasks

Support known possible keys in Object.entries and Object.fromEntries #35745

wucdbm opened this issue Dec 18, 2019 · 9 comments

Comments

@wucdbm
Copy link
Task lists! Give feedback

@wucdbm wucdbm commented Dec 18, 2019

Search Terms

Object.entries, Object.fromEntries

Suggestion

Add

entries<E extends PropertyKey, T>(o: { [K in E]: T } | ArrayLike<T>): [E, T][]; to Object.entries in lib.es2017.object.d.ts see #12253 (comment)

and

fromEntries<K extends PropertyKey, T = any>(entries: Iterable<readonly [K, T]>): { [k in K]: T }; to Object.fromEntries in lib.es2019.object.d.ts
OR
fromEntries<K extends string, T = any>(entries: Iterable<readonly [K, T]>): { [k in K]: T }; extends string for now until #31393 is resolved in terms of the "keyofStringsOnly": true compiler option, which would disallow number and symbol.

#31393 is a related issue that suggests the same addition @ fromEntries
Any other research lead me to #12253 (comment)

Use Cases

Basically, I'd like to map an object with known finite number of fields to an object with the same keys, but where the values are of different type (in the example below - the values are transformed from an object containing label: string and rating: number to number)

Examples

Example repository at https://github.com/wucdbm/typescript-object-entries-key-type
Commenting out the two suggested additions in src/types/es.d.ts leads to two errors in index.ts (Please have a look at the types in src/types/rating.d.ts)

const requestData: BackendRatingRequest = {
    stars: Object.fromEntries(
        Object.entries(rating.stars).map((v: [RatingFields, RatingWithLabel]) => {
            return [v[0], v[1].rating]
        })
    ),
    feedback: rating.feedback
};
  1. Object.entries(rating.stars).map((v: [RatingFields, RatingWithLabel]) => { RatingFields has no sufficient overlap with string, where because of rating.stars's index signature, the key can only be one of the values of RatingFields

  2. Object.fromEntries complains that the keys of RatingFields are missing. But in this case, the first element of the returned array can only be of type RatingFields

I'm leaving the first checklist option unticked. I am unsure whether this wouldn't be a breaking change for TypeScript code in some situations. I personally haven't encountered one, and have had the same es.d.ts file, found in the example repo, in our project, in order to prevent build errors.

Would be nice if someone with more experience in TS's internals had a look at this. Particularly if it woul lead to any regressions.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@MicahZoltu
Copy link
Contributor

@MicahZoltu MicahZoltu commented Dec 18, 2019

The case of Object.fromEntries I believe this is different from the Object.keys and Object.entries problem. In the Object.keys and Object.entries case, it would be incorrect for TypeScript to assume that the only keys on the object are limited to those on the type. In the case of Object.fromEntries however, TypeScript can guarantee that the Object it returns will at least have the set of keys it knows about on the incoming tuple array.

@wucdbm I recommend removing the Object.entries case from this request as that cannot change without breaking TypeScript type safety (see the issue about Object.keys you linked). I think Object.fromEntries can be fixed though.

@dragomirtitian
Copy link
Contributor

@dragomirtitian dragomirtitian commented Dec 18, 2019

Personal stab at typing it, it gets kind of complex, not sure if there is a simpler approach 😕

type UnionToIntersection<T> = (T extends T ? (p: T) => void : never) extends (p: infer U) => void ? U : never
type FromEntries<T extends readonly [PropertyKey, any]> = T extends T ? Record<T[0], T[1]> : never;
type Flatten<T> = {} & {
  [P in keyof T]: T[P]
}

function fromEntries<V extends PropertyKey, T extends [readonly [V, any]] | Array<readonly [V, any]>>(entries: T): Flatten<UnionToIntersection<FromEntries<T[number]>>> {
  return null!;
}

let o = fromEntries([["A", 1], ["B", "1"], [1, true]])
// let o: {
//     A: number;
//     B: string;
//     1: boolean;
// }

Playground Link

Or without any helper types (can't wait for the SO questions as to what this does 😂):

function fromEntries<V extends PropertyKey, T extends [readonly [V, any]] | Array<readonly [V, any]>>(entries: T):
  (((T[number] extends infer Tuple ? Tuple extends [PropertyKey, any] ? Record<Tuple[0], Tuple[1]> : never : never) extends
    infer FE ? (FE extends FE ? ((p: FE) => void) : never) extends (p: infer U) => void ? U : never : never) extends 
    infer R ? { [P in keyof R] : R[P] }: never)
    
  {
  
  return null!;
}

let o = fromEntries([["A", 1], ["B", "1"], [1, true]])
// let o: {
//     A: number;
//     B: string;
//     1: boolean;
// }

Playground Link

@wucdbm
Copy link
Author

@wucdbm wucdbm commented Dec 19, 2019

@MicahZoltu Fair enough.

In that case, I guess the Object.entries problem could be solved by a 3rd-party library that implements runtime extraction based on a type/interface, simply for the sake of not writing these functions by hand.

For example,

import {entries} from 'some-lib';

const someTypeEntriesOnly = entries<SomeType>(object);

would generate (once per type) a function that takes an object and returns the fields of SomeType only by calling Object.entries(object) and then calling .filter to only return the subset contained in SomeType. Or something like that. Assuming the Object.fromEntries proposal is accepted, this would work perfectly well for us, although in our particular case we wouldn't need the .filter overhead from such a library as the values passed around in our app never satisfy two separate types. At least so far.
But then again, this seems to fall outside of the intended use of TypeScript itself.

I stumbled upon https://www.npmjs.com/package/typescript-is and #14419 today. Could use its source code as a starting point if 14419 is accepted and its easy to plug into TS for code generation.

WDYT?

@MicahZoltu
Copy link
Contributor

@MicahZoltu MicahZoltu commented Dec 19, 2019

Something similar to typescript-is could work for generating code that "loops over all of the keys known at compile time, but not over all of the keys on the object".

@wucdbm
Copy link
Author

@wucdbm wucdbm commented Dec 19, 2019

Original comment updated.

Furthermore, due to #31393 imo it makes sense to go with K extends string rather than K extends PropertyKey for the time being, as that shouldn't hurt anybody, until #31393 is resolved.

fromEntries<K extends string, T = any>(entries: Iterable<readonly [K, T]>): { [k in K]: T };

@Wenzil
Copy link

@Wenzil Wenzil commented Aug 5, 2020

What is the status on this? fromEntries seems like it would only benefit from @wucdbm's type signature above.

@MicahZoltu
Copy link
Contributor

@MicahZoltu MicahZoltu commented Aug 6, 2020

Often you can achieve the desired result with a pattern like this:

const fruits = [ 'apple', 'banana', 'cherry' ] as const
type Fruits = (typeof fruits)[number]
type FruitBasket = Record<Fruits, number>

function countFruits(fruitBasket: FruitBasket) {
    let totalFruits = 0
    for (const fruit of fruits) {
        totalFruits += fruitBasket[fruit]
    }
    return totalFruits
}

countFruits({ apple: 5, banana: 7, cherry: 3 }) // returns: 15

const produceBasket = { apple: 5, banana: 2, cherry: 1, asparagus: 7 }
countFruits(produceBasket) // returns: 8; note it didn't count the asperagus
@wucdbm
Copy link
Author

@wucdbm wucdbm commented Aug 6, 2020

@MicahZoltu Good point. That could come in handy in several of the use-cases the .entries typing proposal was trying to solve.

Does anybody know a use-case where fromEntries<K extends string, T = any>(entries: Iterable<readonly [K, T]>): { [k in K]: T }; will be wrong or interfere with other features or break existing code?

@alamothe
Copy link

@alamothe alamothe commented Jan 25, 2021

Can TypeScript at least provide a type-safe version of Object.entries for Record<K, V>?

Here keys are known to be of K, but the current signature treats them as strings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
6 participants