While I was working on a refactor, in the NgRx project, to allow N number of action handlers
on within an NgRx reducer
createReducer - huzzah for variadic tuple types 🥳, I've encountered a TypeScript issue that I've already bumped against to.
The issue is a compile-time error that gives us the message "Argument of type 'interface' is not assignable to parameter of type 'interface'". When you don't read the full error message (like I did) it can take a while to understand the problem, why it happens, and how to solve this error.
From my understanding, the error pops up when there's a wrapper method that:
- uses a generic
- and has a callback argument that works with the same generic
- the callback argument returns a type that contains the same generic
- and the generic types are not 100% identical
The snippet below illustrates the problem in the most simple way.
These functions give us the following results when we use them:
Now the weird part is that when the
callback method accepts an input parameter of the same type, it does compile.
But only when the argument is used.
From what I could see, is that somehow TypeScript isn't able to correctly infer the generic's interface anymore. To be honest with you, I don't know why and I would expect this to compile because the signature has the same types.
As shown in the usage examples above, we can make this work but therefore we must make changes to the signature or the way how the callback method is invoked. From a consumer's perspective, this is bad and this would've been a massive breaking change for the ones using NgRx.
As a fix, I introduced a new generic in the NgRx to help TypeScript with the interference. While at first this seemed like a fix, it introduced a hidden breaking change because the signature of the
on method was changed.
Noteworthy to mention that if you're adding a generic to only use it once, you're probably doing something wrong. This rule "Type Parameters Should Appear Twice" is explained in The Golden Rule of Generics, written by Dan Vanderkam.
The solution to resolve this issue correctly is to help TypeScript to infer the type. For this, we can tweak the strictness of the generic. Instead of using the generic directly we can create a mapped type by using
The other strange part here is that in the NgRx types, Alex didn't need to type the generic's properties as potentially
undefined (as you can see here) while I had to when I created this reproduction.
So while this blog post does leave us with some unclarity, it does provide a solution to this problem. To play around with this reproduction, see the TypeScript Playground link
Please consider supporting me if have you enjoyed this post and found it useful: