-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Comments
The case of @wucdbm I recommend removing the |
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;
// } 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;
// } |
@MicahZoltu Fair enough. In that case, I guess the For example,
would generate (once per type) a function that takes an object and returns the fields of 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? |
Something similar to |
Original comment updated. Furthermore, due to #31393 imo it makes sense to go with
|
What is the status on this? |
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 |
@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 |
Can TypeScript at least provide a type-safe version of Here keys are known to be of K, but the current signature treats them as strings. |
Just now I was thinking: type A = {
Message: string;
Detail: string;
code: string;
}
Object.entries(a) // should return B
type B = [
["Message", string],
["Detail", string],
["code", string],
]; Is that the same feature that this issue is requesting? I was about to make a feature request issue for this use-case. |
Hi, I was just bitten by this. I very much expected this to work: type Foo = 'a' | 'b' | 'c'
const foos: Foo[] = ['a', 'b', 'c']
const recs: Record<Foo, number> = Object.fromEntries(foos.map((foo, i) => [foo, i])) |
At least this should work: let obj = { a: 1, b: 2 };
obj = Object.fromEntries(Object.entries(obj)); Its pattern is generally used as a quick implementation of mapValues. Please fix it. |
Do |
Ok. Then lets make this work let obj = { a: 1, b: 2 };
obj = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value + 1])); |
@wucdbm ? |
@simPod It has been too long and there are several objections against this, and I've already forgotten whether anything could be done on this matter. I know for sure I made a custom type on one of my projects and that's been long forgotten. If you have the capacity to carry on, please review and open a new discussion about the part that could be implemented, and check if there aren't any objections against it. To be fair, given the time span and the fact that JS isn't my primary strength, I can't judge what makes sense at this point, and I don't really have the time or will to get into it. This has been further fortified by several rejections of contributions towards improving terrible (to say the least) design of (otherwise great) OS libraries, so my will to help anywhere is nowhere to be found these days, sorry. |
Are there any reasons against making a PR with @dragomirtitian’s types in #35745 (comment)? |
It's not correct to use the input type to determine the output type, because knowing what might be in an array is not the same as knowing what's actually in it. This program is legal per the above definitions but unsound: const arr: Array<["A", 1] | ["B", 2]> = [];
let o = fromEntries(arr);
let m: number = o.A; |
@RyanCavanaugh this is true for writeable arrays, but not for const tuples. |
So I get the hesitancy to use const property keys here. However, I don't understand why the return type is After all, |
Just FYI for anyone following, since my PR with strict typing wasn't accepted due to "too complex", I've finally gotten around to publishing an NPM package with the strict Object.fromEntries typings. It doesn't suffer from any unsoundness as far as I can tell and is fully compatible with the regular Object.fromEntries. See nesity-types. |
The |
@wucdbm Neither |
This code is legal in TypeScript today, even though Object.fromEntries really should return |
here is an easy solution for constant entries: const fromEntries: <T extends readonly [keyof any, any][]>(entries: T) => T extends T ? { [E in T[number] as E[0]]: E[1] } : never = Object.fromEntries
const x = fromEntries(true
? [['a', 1], ['b', 2]] as const
: [['a', 1], ['b', 3], ['c', 4]] as const)
const y = x.a // 1
const z = x.b // 2 | 3
const w = x.c // error
const v = 'c' in x ? x.c : null // 4 | null the oh and here is one for // https://stackoverflow.com/a/55128956
// oh boy don't do this
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never
type TuplifyUnion<T> = [T] extends [never] ? [] : [...TuplifyUnion<Exclude<T, LastOf<T>>>, LastOf<T>]
type Entries<T extends {}, K = TuplifyUnion<keyof T>> = K extends readonly (keyof any)[] ? [...{ [I in keyof K]: [K[I], K[I] extends keyof T ? T[K[I]] : never] }] : never
export const entries: <T extends {}>(obj: T) => Entries<T> = Object.entries as any |
@LoganDark thank you! i think this is slightly safer in that it forces a tuple to be passed, which avoids the unsoundness issues mentioned above. It also allows you to pass the tuple inline without adding export const fromEntriesTuple: <
const T extends readonly [readonly [keyof any, any], ...Array<readonly [keyof any, any]>],
>(
entries: T,
) => T extends T ? { [E in T[number] as E[0]]: E[1] } : never = Object.fromEntries;
const c = fromEntriesTuple([
['a', 1],
['b', 2],
]); // // { a: 1, b: 2 }, infers a const tuple
const d = fromEntriesTuple([['a', 1]]); // { a: 1 }
const aKnownTuple = [
['a', 1],
['b', 2],
] as const;
const a = fromEntriesTuple(aKnownTuple); // { a: 1, b: 2 }
const anArray = [['a', 1]];
const e = fromEntriesTuple(anArray); // error, as expected |
I think here's an // https://stackoverflow.com/a/55128956
// oh boy don't do this
type UnionToIntersection<K> = (K extends any ? (k: K) => void : never) extends ((k: infer I) => void) ? I : never
type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R extends T ? R : never : never
type TuplifyUnion<T> = [T] extends [never] ? [] : [...TuplifyUnion<Exclude<T, LastOf<T>>>, LastOf<T>]
type GeneralKeys<T> = [T] extends [never] ? never : (keyof any extends infer A ? A extends LastOf<T> ? T : never : never) | GeneralKeys<Exclude<T, LastOf<T>>>
type SpecificKeys<T> = Exclude<T, GeneralKeys<T>>
type TuplifyKeys<T> = [...TuplifyUnion<SpecificKeys<T>>, ...([any] extends [GeneralKeys<T>] ? GeneralKeys<T>[] : [])]
type TuplifyArrayKeys<T> = TuplifyKeys<Exclude<keyof T, keyof []>>
type Entries<T extends {}> = T extends any ? (T extends [...any] ? TuplifyArrayKeys<T> : TuplifyKeys<keyof T>) extends infer K ? { [I in keyof K]: [K[I], K[I] extends keyof T ? T[K[I]] : never] } : never : never
export const entries: <T extends {}>(obj: T) => Entries<T> = Object.entries as any it's silly. I'm so autistic lmao |
Search Terms
Object.entries, Object.fromEntries
Suggestion
Add
see #12253 (comment)entries<E extends PropertyKey, T>(o: { [K in E]: T } | ArrayLike<T>): [E, T][];
to Object.entries inlib.es2017.object.d.ts
and
fromEntries<K extends PropertyKey, T = any>(entries: Iterable<readonly [K, T]>): { [k in K]: T };
to Object.fromEntries inlib.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 thesame keys
, but wherethe 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 insrc/types/rating.d.ts
)Object.entries(rating.stars).map((v: [RatingFields, RatingWithLabel]) => {
RatingFields has no sufficient overlap with string, where because ofrating.stars
's index signature, the key can only be one of the values of RatingFieldsObject.fromEntries
complains that the keys ofRatingFields
are missing. But in this case, the first element of the returned array can only be of typeRatingFields
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:
The text was updated successfully, but these errors were encountered: