1

I have this:

export function reduceByProp<T, Y extends keyof T>(
  array: T[],
  mapper: (a: T) => Y
): { [key: Y]: T } {
  return array.reduce(
    (previous: T, current: T) => ({ ...previous, [mapper(current)]: current }),
    {}
  );
}

but TypeScript is unhappy with [key: Y] because an index has the be a string or a number. But since Y is a key of T it's by default also a string or a number right?

albertjan
  • 7,739
  • 6
  • 44
  • 74
  • No, `keyof` isn't the same as an indexed signature. `keyof` can be a union of literals, which isn't either `string` or `number`. For instance: https://www.typescriptlang.org/play/index.html?ssl=7&ssc=10&pln=7&pc=13#code/JYOwLgpgTgZghgYwgAgGIHt3IN4ChnJwBcyAzmFKAOYDc+yARieZSLfQsxdXQL65gAngAcUGdAGkIg5AF5kAa2noYaTHQToQ5ZAA8S4qTPkAiACYmaQA – T.J. Crowder Feb 05 '20 at 18:18
  • 1
    Can you show a sample call you're trying to make possible with `reduceByProp`? – T.J. Crowder Feb 05 '20 at 18:19
  • what does [key: Y] mean? is that meant to be an object or an array? – Rick Feb 05 '20 at 18:20
  • I'm not saying it's any string I'm saying that since it's used as a key it should be possible to use it as a key again. – albertjan Feb 05 '20 at 18:20
  • @albertjan - `{[key: Y]: T}` is defining an index signature. In index signatures, the key's type must be `string` or `number`. `keyof` has more nuance than that. – T.J. Crowder Feb 05 '20 at 18:21
  • @T.J.Crowder I'm not stuggling to get it to work. I'm wondering why `Y extends key T` is not a valid `index` type. – albertjan Feb 05 '20 at 18:22
  • @albertjan - Well, that's why. Index signatures must use `string` or `number`. `keyof T` is not necessarily `string` or `number`. (BTW: "Look at the code" is ***really*** easy to read as being rude. You probably didn't mean to seem rude there, so just a head's up.) – T.J. Crowder Feb 05 '20 at 18:23
  • when is `keyof T` not a number? – albertjan Feb 05 '20 at 18:23
  • @Rick I was highlighting the bit of the code that is failing for me. it's a definition of an index type. – albertjan Feb 05 '20 at 18:25
  • 1
    Not sure if it's what's at play here, but this doc says `keyof` produces `string | number | symbol`, so symbol could be messing it up. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html – Brian Nickel Feb 05 '20 at 18:25
  • 1
    Possible duplicate of [Combining generics with index type](https://stackoverflow.com/a/59331699/2887218) – jcalz Feb 05 '20 at 18:27
  • @jcalz - That does seem to cover it. I notice you didn't use your dupehammer, so I've held off doing so as well (I'm not at all sure I should have a TypeScript dupehammer :-) ) to defer to your judgement. – T.J. Crowder Feb 05 '20 at 18:30
  • 1
    It's easy for me to tell you to change `{[key: Y]: T}` to `{[K in Y]: T}` but I'm not sure about the rest of your code, since the types seem off to me. I'd change it to [this](http://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABAJwKYBMQVQIQJ4AKycADgDwAqANIgNKKoAeUqY6AzokaaslHrVR4AfAAoAUIkQBDZMml4AXIgoBtALpVJiALbSSJXstHTlFAJSIAvMLrjLAb21ooIZElny8AOjSZsElJSoiRoAG7wIOw0EG5oYFCWNoiiDojeGaGoEXBRNKp6BryisXKsierKpfFQiAC+5lpBiA51MpxpqgSIMEi06gD8ZvXa5gDc4nXiQA) unless you have a reason why, for example, you want to limit `Y` to be `keyof T` and not just any key at all. – jcalz Feb 05 '20 at 18:31
  • @T.J.Crowder I don't think this can be closed as a duplicate of that because there are no upvotes there. I might be able to find another one though; pretty sure I've answered this part before – jcalz Feb 05 '20 at 18:33
  • I didn’t know about `in` I was just surprised that it didn’t accept `Y` as a valid index. – albertjan Feb 05 '20 at 18:35
  • Possible duplicate of [Typescript literal types used as key to indexer](https://stackoverflow.com/questions/49091528/what-would-the-type-definition-look-like-for-a-function-that-returns-an-object-t?rq=1) – jcalz Feb 05 '20 at 18:40
  • 1
    @jcalz - It can be now. :-) Or of course that second one. – T.J. Crowder Feb 05 '20 at 18:41

1 Answers1

1

I suggest you change your code to look like this:

function reduceByProp<T, K extends PropertyKey>(
  array: T[],
  mapper: (a: T) => K
) {
  return array.reduce(
    (previous, current) => ({ ...previous, [mapper(current)]: current }),
    {} as { [P in K]?: T }
  );
}

Explaining the differences:

  • For your question, you can't do {[key: K]: T} or the like, since index signatures are constrained to be all strings or all numbers. Instead you can use mapped types of the form {[P in K]: T}.

  • Unless you want reduceByProp([{foo: 1}], v => "bar") to fail, you should make K extends PropertyKey and not K extends keyof T. keyof T is specifically only the keys inside the objects in your array, while PropertyKey is any key you want.

  • Don't annotate previous and current, or if you do annotate them, don't annotate them as T. current is definitely T, but previous is an accumulator and is not T but the return type of reduceByProp() which is something whose keys are returned by mapper() and whose value types are T.

  • Give the initial reduce object {} an explicit type, or otherwise specify what reduce() is expected to produce. The value {} will be inferred as type {} otherwise, which will fail to type check. So I've given it {} as ....

  • I've made the return type {[P in K]?: T} in which the properties are optional (?) instead of required as in {[P in K]: T}. The reason is that you might want to make a call like this:

    reduceByProp([{ foo: 1 }, { foo: 3 }, { foo: 5 }], v => v.foo % 2 === 0 ? "even" : "odd");
    

    The return type of that in my version is {even?: {foo: number}, odd?: {foo: number}}. It's good that those are optional because it turns out that the output has no even key at all.

Okay, hope that helps; good luck!

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360