Search code examples
typescripttypescript-generics

Typescript Key-Value relation preserving Object.entries type


The typing for Object.entries provided by typescript has the return type [string, T][] but I am searching for a generic type Entries<O> to represent the return value of this function that keeps the relationship between the keys and the values.

Eg. when having an object type like

type Obj = {
    a: number,
    b: string,
    c: number
}

I'm looking for a type Entries<O> that results in one of the types below (or something similar) when provided with Obj:

(["a", number] | ["b", string] | ["c", number])[]
[["a", number], ["b", string], ["c", number]]
(["a" | "c", number] | ["b", string])[]

That this isn't correct for all use cases of Object.entries (see here) is no problem for my specific case.


Tried and failed solution:

type Entries<O> = [keyof O, O[keyof O]][] doesn't work for this as it only preserves the possible keys and values but not the relationship between these as Entries<Obj> is ["a" | "b" | "c", number | string].

type Entry<O, K extends keyof O> = [K, O[K]]
type Entries<O> = Entry<O, keyof O>[]

Here the definition of Entry works as expected eg. Entry<Obj, "a"> is ["a", number] but the application of it in the second line with keyof O as the second type variable leads again to the same result as the first try.


Solution

  • Here's a solution, but beware when using this as a return type for Object.entries; it is not always safe to do that (see below).


    When you want to pair each key with something dependent on that key's type, use a mapped type:

    type Entries<T> = {
        [K in keyof T]: [K, T[K]];
    }[keyof T][];
    
    type Test = Entries<Obj>;
    // (["a", number] | ["b", string] | ["c", number])[]
    

    An alternative solution using a distributive conditional type: this requires K to be a "naked type parameter", hence the extra optional generic parameter.

    type Entries2<T, K extends keyof T = keyof T> =
        (K extends unknown ? [K, T[K]] : never)[]
    

    The second version, which has a tuple type containing the properties instead of a union, is much harder to construct; it is possible to convert a union to a tuple but you basically shouldn't do it.

    The third version is manageable, but a bit more complicated than the first version: you need PickByValue from this answer.

    type Entries3<T> = {
        [K in keyof T]: [keyof PickByValue<T, T[K]>, T[K]]
    }[keyof T][];
    
    type Test3 = Entries3<Obj>;
    // (["a" | "c", number] | ["b", string])[]
    

    Playground Link


    I guess I should also explain why Typescript doesn't give a stronger type to Object.entries. When you have a type like type Obj = {a: number, b: string, c: number}, it's only guaranteed that a value has those properties; it is not guaranteed that the value does not also have other properties. For example, the value {a: 1, b: 'foo', c: 2, d: false} is assignable to the type Obj (excess property checking for object literals aside).

    In this case Object.entries would return an array containing the element ['d', false]. The type Entries<Obj> says this cannot happen, but in fact it can happen; so Entries<T> is not a sound return type for Object.entries in general. You should only use the above solution with Object.entries when you yourself know that the values will have no excess properties; Typescript won't check this for you.