Skip to content

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

Closed

Description

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.

Activity

  1. MicahZoltu commented on Dec 18, 2019

    @MicahZoltu

    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.

    Martin Kirilov (@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.

  2. dragomirtitian commented on Dec 18, 2019

    @dragomirtitian
    Contributor

    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

  3. wucdbm commented on Dec 19, 2019

    @wucdbm
    Author

    Micah Zoltu (@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?

  4. MicahZoltu commented on Dec 19, 2019

    @MicahZoltu

    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".

  5. wucdbm commented on Dec 19, 2019

    @wucdbm
    Author

    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 };

  6. Wenzil commented on Aug 5, 2020

    @Wenzil

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

  7. MicahZoltu commented on Aug 6, 2020

    @MicahZoltu

    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
  8. wucdbm commented on Aug 6, 2020

    @wucdbm
    Author

    Micah Zoltu (@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?

  9. alamothe commented on Jan 25, 2021

    @alamothe

    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.

  10. sdegutis commented on Aug 20, 2021

    @sdegutis

    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.

  11. flying-sheep commented on Nov 4, 2021

    @flying-sheep

    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]))
  12. CarterLi commented on Nov 26, 2021

    @CarterLi

    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.

  13. wucdbm commented on Nov 27, 2021

    @wucdbm
    Author

    Do const obj = {...another} then, should be good
    This is pretty important when mapping the values to something else

  14. CarterLi commented on Nov 27, 2021

    @CarterLi

    Ok. Then lets make this work

    let obj = { a: 1, b: 2 };
    obj = Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, value + 1]));
  15. simPod commented on Jan 20, 2022

    @simPod
  16. wucdbm commented on Jan 21, 2022

    @wucdbm
    Author

    Simon Podlipsky (@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.

  17. flying-sheep commented on Jan 21, 2022

    @flying-sheep

    Are there any reasons against making a PR with Titian Cernicova-Dragomir (@dragomirtitian)’s types in #35745 (comment)?

  18. RyanCavanaugh commented on May 31, 2022

    @RyanCavanaugh
    Member

    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;
  19. niieani commented on Aug 6, 2022

    @niieani

    Ryan Cavanaugh (@RyanCavanaugh) this is true for writeable arrays, but not for const tuples.
    Const tuples by definition must include all the the members, and thus guarantee their contents.
    Here's a PR with a fix: #50203.

  20. GravlLift commented on Feb 21, 2023

    @GravlLift

    So I get the hesitancy to use const property keys here. However, I don't understand why the return type is {k: string]: T}. Given the input type of Iterable<readonly [PropertyKey, T]>, I would expect a return type of {[k: PropertyKey]: T}, no?

    After all, Object.fromEntries([[0, "test"]]) spits out {0: "test"}.

  21. niieani commented on Feb 22, 2023

    @niieani

    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.

  22. wucdbm commented on Feb 23, 2023

    @wucdbm
    Author

    The radash library has mapEntries in case anyone stumbles upon this.

  23. adroste commented on Feb 2, 2024

    @adroste

    Martin Kirilov (@wucdbm) Neither radash nor lodash nor remeda offer proper type inference (for const values with custom keys).
    The only working solutions I've found so far (for both directions entries and fromEntries) is from StackOverflow https://stackoverflow.com/a/69019874/6292230
    Quick reminder: code from SO is licensed CC BY-SA (copyleft/viral)

  24. mariusGundersen commented on Jun 19, 2024

    @mariusGundersen

    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 = Object.fromEntries(arr);
    let m: number = o.A;

    This code is legal in TypeScript today, even though o.A is actually undefined. So the current definition of Object.fromentries is unsound.

    Object.fromEntries really should return Partial<{ [k: PropertyKey]: T; }>, since it's not certain what array entries are there.

  25. LoganDark commented on Aug 25, 2024

    @LoganDark

    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 T extends T is needed to distribute over union types

    oh and here is one for Object.entries:

    // 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
  26. scamden commented on Sep 9, 2024

    @scamden

    LoganDark (@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 as const:

    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
  27. LoganDark commented on Sep 9, 2024

    @LoganDark

    I think here's an entries that works with potentially unbounded arrays and things like that:

    // 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions