4

General Typescript question say I iterate over an array which i know its content and apply a reduce to get an object back which I do know the type for instance:

interface IMyInterface {
  a: number;
  b: number;
  c: number;
}
const result: IMyInterface = ['a','b','c'].reduce((acc: Partial<IMyInterface>,val)=>({...acc,[val]: 1}), {});

Now that wont work because result is expected to be Partial<IMyInterface> which makes sense, considereing TS cant tell the content of the array will produce the "full" object. However What do I need to do so that result can be of type IMyInterface without the need of as IMyInterface ?

Here is a repl https://repl.it/@Sudakatux/KaleidoscopicGraciousApplicationpackage

Thanks in advance

jstuartmilne
  • 4,398
  • 1
  • 20
  • 30
  • With `reduce()`? There's probably nothing better than a type assertion; in order for this to work, the callback would need to be generic `>(acc: T, val: K): T & Record => ...` but the type system lacks the ability to express the higher-order operation of specifying `T` and `K` for each element of the tuple type `["a","b","c"]`. – jcalz Nov 21 '19 at 19:27
  • You can [unroll](http://www.typescriptlang.org/play//#code/JYOwLgpgTgZghgYwgAgLIE8CSyDeAoZQ5OALmRAFcBbAI2gG4CiazLaGnCFXq6pGAvnhgUQCMMAD2IZDAAUADzIZMASlzIhYdAAcUAUQU64IACYAeACoA+ZAF5khsFERgryCAshmAzslAw0MgAqsgA-BoA2gDS-jIA1hDokjAhALpkwTFpmsisEABu0AA0yDaMImIS0sg+EGAAClCSOlalsZ7epn6JyamWpQBqHl4QvmXZ1nKSNABWZAPIvWTRpQVwADZkg6oknMQ+dVBgfjOz-n6GxmbuAGTIAEoQCJJQFqvIg9a2+ETI03MDsQQOggU8Xm9zB8vqpIr0cg51htBHgNvVkFAID4KBswGQGnBjsBNuYVLYHDgBBU5JjsbjVPRkAB6JkeKDNKB4OqNZo6GlYnFgUoAIjgwtKAEYGcJ+XSwAzmazoByufUmi1ZYKRTRxcgpdTaYKFSy2Srueq+YbcSKELr9TKrfLGSbJPE4Og8J6gA) it, but not in a loop or `reduce()`. – jcalz Nov 21 '19 at 19:33
  • @jcalz I dont understand how thats related. sorry. and in the unroll example there is a cast to any and then a cast from any to Record – jstuartmilne Nov 21 '19 at 20:08
  • 1
    That's not [important](http://www.typescriptlang.org/play//#code/JYOwLgpgTgZghgYwgAgLIE8CSyDeAoZQ5OALmRAFcBbAI2gG4CiazLaGnCFXq6pGAvnhgUQCMMAD2IZDAAUADzIZMASlzIhYdAAcUAUQU64IACYAeACoA+ZAF5khsFERgryCAshmAzslAw0MgAqsgA-BoA2gDS-jIA1hDokjAhALpkwTFpmsisEABu0AA0yDaMImIS0sg+EGAAClCSOlalsZ7epn6JyamWpQBqHl4QvmXZ1nKSNABWZAPIvWTRpQVwADZkg6oknMQ+dVBgfjOz-n6GxmbuAGTIAEoQCJJQFqvIg9a2+ETIZ5FejkHOsNoI8Bt6sgoBAfBQNmAyA04MdgJtzCpbA4cAIKnIYXCEap6MgAPSkjxQZpQPB1RrNHT42HwsClABEcDZpQAjMThEzCWBiWSKdBqbT6k0WgKWeyaFzkLy8QSWcLyZTxXSpYyVQj2QgFUr+bqhST1ZJ4nB0HgbUA) to the unroll example, sorry – jcalz Nov 21 '19 at 20:13
  • My point was that there's no way to tell the compiler how `reduce()` forms a chain of accumulator types that change from one loop to the next. The generic callback type can at least represent the change of type, which works if you unroll it via `setProp()` or something like that, but there's no way to give a type signature to `reduce()` or any loop structure that will understand that you start with `{}`, then have `{a: number}`, then have `{a: number, b: number}`, and finally have `IMyInterface`. Each step is representable, but the whole thing is not. Type assertions are probably necessary. – jcalz Nov 21 '19 at 20:16
  • 1
    Here's a more [analogous unrolled example](http://www.typescriptlang.org/play//#code/JYOwLgpgTgZghgYwgAgJIFkCerzXk5AbwChkzk4AuZEAVwFsAjaAblPMerqdfbIS4NmUNgF9iMWiARhgAexDIYACgAe1DNlyxEEAJRFk4hAoDOYZAkbIAvH2QAeANLIIqyCAAmp5AGsImHIwaFg4kDpIADTIACqu7hBePgAKcFCycAA2DppheLoAfAXKiAKx0QBuWdROepT25LHIAGTIAEoQJlCeztHcwgW2g8qEyAB0E6XRANpVmQC61ACMRnrEEspWm4zbI6LRAERwB3qHjCeHCCd6LI3EQA). That just works, but only because the compiler sees each step happen separately. – jcalz Nov 21 '19 at 20:21
  • Thanks for the example, was kind of what i was looking for – jstuartmilne Nov 21 '19 at 20:34
  • Why the downvote. The question seems pretty clear. and the answers go to the point? – jstuartmilne Jan 06 '20 at 16:58

2 Answers2

6

The short answer here is: you pretty much need to use a type assertion because it's not possible to have the compiler figure out that what you're doing is safe.


The much longer answer: in order to even begin to let the compiler know what's going on, you need the callback to be generic. Here's one way to type it:

const cb = <K extends keyof IMyInterface, T extends Partial<IMyInterface>>(
    acc: T, val: K): T & Record<K, number> => ({ ...acc, [val]: 1 })

That type signature says that the cb takes two parameters, acc and val. The acc parameter is of generic type T which must be assignable to Partial<IMyInterface>, and the val parameter is of generic type K which must be assignable to keyof IMyInterface. Then the output of the callback is T & Record<K, number>: that is, it is an object with all the keys and values from T, but it also has a definite number value at the key K. So when you call cb(), the return value is potentially of a different type from that of acc.

This gives enough information to the compiler to allow you to avoid type assertions... but only if you perform the reduce()-like operation with cb() manually, by unrolling the loop into a bunch of nested calls:

const result: IMyInterface = cb(cb(cb({}, "a"), "b"), "c"); // okay
const stillOkay: IMyInterface = cb(cb(cb({}, "a"), "c"), "b"); // okay
const mistake: IMyInterface = cb(cb(cb({}, "b"), "b"), "c"); // error! property "a" is missing

Here you can see that the compiler is really looking out for you, since if you call cb() in the wrong way, you get an error telling you so.

Unfortunately, the type signature for Array<T>.reduce(),

reduce<U>(
  callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U, 
  initialValue: U
): U;

is insufficient to represent the successive type narrowing that happens each time callbackfn is called on elements of the array. And as far as I can tell, there's no way to alter it to do this. You want to say that the callbackfn type is some crazy intersection of types corresponding to how it behaves for each successive member of the array, like ((p: A, c: this[0])=>B) & ((p: B, c: this[1])=>C) & ((p: C, c: this[2])=>D) & ..., for generic parameters A, B, C, D, etc., and then hope that the compiler can infer these parameters from your call to reduce(). Well, it can't. The kind of higher order inference just isn't part of the language (at least as of TS3.7).

So, that's where we have to stop. Either you can unroll the loop and call cb(cb(cb(..., or you call reduce() and use a type assertion. I think the type assertion really isn't all that bad; it's meant specifically for situations in which you are smarter than the compiler... and this seems to be one of those times.

Okay, hope that helps; good luck!

Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

I've run into the same thing and have been able to get things working as needed by casting the acc as the desired interface, and the initial value as any:

const result= ['a','b','c'].reduce((acc: IMyInterface,val)=>({...acc,[val]: 1}), {} as any);

This should set result to be of type IMyInterface without raising tslint errors.

Mickey
  • 570
  • 3
  • 11
  • thanks for the response. So im looking for something that does not require casting. That includes casting to any – jstuartmilne Nov 21 '19 at 19:29
  • The funny thing is. it would be actualy wrong because on the first iteration acc is empty and we are saying there is type IMyInterface which is not it is actually a Partial – jstuartmilne Nov 21 '19 at 19:44
  • 1
    Yes, technically it's wrong inside the reducer, but this has done the job when used in these specific cases, where I'm transforming a known quantity with a very simple, terse reducer. Since the function is so short, I've given up on finding a more "correct" solution - I just care about the final result being typed correctly. In more complicated cases, though, I'd agree that having the correct typing inside the function would be great. – Mickey Nov 21 '19 at 19:53