typescripttypescript-genericsunion-types

How to take union of objects with keys instead of using union of key type in object? (Distribute keys over objects)


Consider the following function (playground link):

function objectify<T extends string>(o: {[key in T]: number}) {
    const output = {} as {[key in T]: {value: number}};
    for (const key in o) {
        output[key as T] = {value: o[key as T]}
    }
    return output
}

Now, I have two issues.

  1. TypeScript thinks that the argument to objectify must have all possible incoming keys instead of just only incoming keys:
const data = {
    first: { a: 1 },
    second: { b: 2 }
}

function objectifyDatum(k: keyof typeof data) {
    // Argument of type '{ a: number; } | { b: number; }'...
    return objectify(data[k])
    // is not assignable to parameter of type '{ a: number; b: number; }'.
}
  1. The return type of objectify is similarly an object with all keys present in the incoming union type, instead of a union of objects.
// has type { a: { value: number }; b: { value: number } }, 
// but I want { a: { value: number } } | { b: { value: number } }
const objectifiedDatum = objectifyDatum('first')

These seem like similar issues, revolving around the fact that TypeScript is merging the keys in the union { a: number } | { b: number } into one larger object with keys a and b.

The solution to this would be to “distribute” the key type over a union of object types, but I don't know how to tell TypeScript this is what I want.


Solution

  • The issue is related to the generic type, because you are not passing a generic object but you are using the generics on the keys of such object.

    A simple solution is to pass directly the generic object:

    function objectify<T extends { [Key: string]: number }>(o: T) {
        const output = {} as {[key in keyof T]: {value: number}};
        for (const key in o) {
            output[key] = { value: o[key] }
        }
        return output
    }
    

    With this objectifiedDatanum is correctly typed as

    const objectifiedDatum: {
        a: {
            value: number;
        };
    } | {
        b: {
            value: number;
        };
    }