Typescript: `Record<T[K], T[]>` does not compile anymore

Created on 15 May 2020  路  5Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.9.2


Search Terms:
Record
Symbol

Code

const groupArrayByObjectProperty = <T extends any, K extends keyof T>(
    array: T[], attributeGetter: (item: T) => number | string,
): Record<T[K], T[]> => array.reduce(
        (previous, current) => {
            const value = attributeGetter(current);
            previous[value] = (previous[value] || []).concat(current);
            return previous;
        },
    {} as Record<T[K], T[]>,
    );

console.log(groupArrayByObjectProperty([{a: 'a', b: 'b'}], (x) => x.a));

The function's purpose is to create an object from an array of objects, grouped by a value of the objects.

[
    {a: 1, b: '2', c: 'e'},
    {a: 2, b: '0', c: 'x'},
    {a: 2, b: '2', c: 'p'},
    {a: 1, b: '2', c: 'e'},
    {a: 1, b: '0', c: 'c'},
    {a: 3, b: '0', c: 't'},
]

When grouped by the value of a becomes:

{
    1: [
        {a: 1, b: '2', c: 'e'},
        {a: 1, b: '2', c: 'e'},
        {a: 1, b: '0', c: 'c'},
    ],
    2: [
        {a: 2, b: '0', c: 'x'},
        {a: 2, b: '2', c: 'p'},
    ],
    3: [
        {a: 3, b: '0', c: 't'},
    ],
},

The important aspect of this function is that the typing of the output object is correct.

Expected behavior:
The code compiles and works like it did on TS 3.8

Actual behavior:
I'm now getting these errors:

src/functions/group-array-by-object-property.ts:8:11 - error TS2344: Type 'T[K]' does not satisfy the constraint 'string | number | symbol'.
  Type 'T[keyof T]' is not assignable to type 'string | number | symbol'.
    Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'string | number | symbol'.
      Type 'T[string]' is not assignable to type 'string | number | symbol'.
        Type 'T[string]' is not assignable to type 'symbol'.
          Type 'T[keyof T]' is not assignable to type 'symbol'.
            Type 'T[K]' is not assignable to type 'symbol'.
              Type 'T[keyof T]' is not assignable to type 'symbol'.
                Type 'T[string] | T[number] | T[symbol]' is not assignable to type 'symbol'.
                  Type 'T[string]' is not assignable to type 'symbol'.

8 ): Record<T[K], T[]> => array.reduce(
            ~~~~

src/functions/group-array-by-object-property.ts:11:13 - error TS7053: Element implicitly has an 'any' type because expression of type 'string | number' can't be used to index type 'Record<T[K], T[]>'.
  No index signature with a parameter of type 'string' was found on type 'Record<T[K], T[]>'.

11             previous[value] = (previous[value] || []).concat(current);
               ~~~~~~~~~~~~~~~

src/functions/group-array-by-object-property.ts:11:32 - error TS7053: Element implicitly has an 'any' type because expression of type 'string | number' can't be used to index type 'Record<T[K], T[]>'.
  No index signature with a parameter of type 'string' was found on type 'Record<T[K], T[]>'.

11             previous[value] = (previous[value] || []).concat(current);
                                  ~~~~~~~~~~~~~~~

src/functions/group-array-by-object-property.ts:14:18 - error TS2344: Type 'T[K]' does not satisfy the constraint 'string | number | symbol'.
  Type 'T[K]' is not assignable to type 'symbol'.

14     {} as Record<T[K], T[]>,
                    ~~~~

Did something change in the TS compiler that broke it? Or should it never have worked from the start? If so, what would have been the correct typing?

Playground Link:
https://www.typescriptlang.org/play/#code/MYewdgzgLgBA5gJxAVwA4EEEIIYE8BCuA8gEYBWApsFAApKoUJS4wC8MAPACowUAeUCmAAmEGNjC4ANDADSvAUNEwA1hVwgAZjC4A+ABQAoGCfFY8ALh0BtALozsUKAgCWJZIIDiFJ4yv6XQQBbKy4ASjZdGDBkIJJGGAAfGGhXMDgpQzCrACUqEARhbmtZextbKNYo7HNcADoECmFkYAojUw6YfVRGgDcXFAgZYGQsISgIqpgAb2NO+dBIWF7sABtkCjZxJ1d3Lx9BBH0RsbAJgG45+Y6ein7B6xX1ilst7r6B5AhHtY3XxOSdjCdUWwEcx1GjTOYUu106jSgozAMFu9y+sOuAF9Mh1ppjxGI8qBCsVSjIuHZdDjTDDDIZFhAQKsKHVViA4PpECgMLVCKRKNQ6CAGExcPprNNsFYAOTYaUyEgykjSzFlfR8SZRPh1bBhGFAA

Related Issues:

37642

Most helpful comment

The change was introduced here #29571.

This might work:

const groupArrayByObjectProperty = <T extends Record<string, string>, K extends keyof T>(
    array: T[], attributeGetter: (item: T) => T[K],
): Record<T[K], T[]> => array.reduce(
        (previous, current) => {
            const value = attributeGetter(current);
            previous[value] = (previous[value] || []).concat(current);
            return previous;
        },
    {} as Record<T[K], T[]>,
    );

All 5 comments

I think this can be demonstrated as short as below.

let record: Record<number, string> = {}
let record2: Record<'abc'|'def', number> = {} // error here

Playground Link

You broke it!

I think this can be demonstrated as short as below.

let record: Record<number, string> = {}
let record2: Record<'abc'|'def', number> = {} // error here

But that error is correct! {} is missing the two properties abc and def.
The first line is most likely an exception because you can never provide a value for all possible keys of Record<number, string>

The change was introduced here #29571.

This might work:

const groupArrayByObjectProperty = <T extends Record<string, string>, K extends keyof T>(
    array: T[], attributeGetter: (item: T) => T[K],
): Record<T[K], T[]> => array.reduce(
        (previous, current) => {
            const value = attributeGetter(current);
            previous[value] = (previous[value] || []).concat(current);
            return previous;
        },
    {} as Record<T[K], T[]>,
    );

I think I live with casting to as Record<'abc'|'def'>. Maybe we should add control flow for detecting undefined value for some keys.

Thank you very much, @jack-williams. That change seems to do the trick.

Was this page helpful?
0 / 5 - 0 ratings