Reputation: 1844
I use TypeScript with React and useReducer and I want to define reducer Actions in a type-safe way.
The simplest approximation of Action is:
type Action = {name : string, payload : any}
The more precise version requires union types:
type Action =
| {name : "setColumns", payload: string[]}
| {name : "toggleColumn", payload: string}
...
So far so good. Then I want to define components which depend on Action or rather on its derivative React.Dispatch<Action>
. There are two ways to do that:
The approach 1) is more type-safe in theory yet much more verbose and complex in practice. The approach 2) can be a good balance between safety and complexity.
Example of Pager
component props in both styles:
// 1)
export type PagerProps1 <Page extends number, Limit extends number> = {
page : Page // -- narrower types
limit : Limit
pagesTotal : number
}
// 2)
export type PagerProps2 = {
page : number // -- wider types
limit : number
pagesTotal : number
}
^ now it's possible to define Pager2
and move it to the library with no dependency on Page
and Limit
which are app-specific. And without generics. That was the intro to provide necessary context.
The problem comes with React.Dispatch
. Here's the test-case that imitates reusal of generic dispatch in place where more precise version is present:
type Action =
| {name : "setColumn"}
| {name : "toggleColumn"}
type OpaqueAction1 = {name : any} // will work
type OpaqueAction2 = {name : string} // will not work
type Dispatch = React.Dispatch<Action>
type OpaqueDispatch1 = React.Dispatch<OpaqueAction1> // will work
type OpaqueDispatch2 = React.Dispatch<OpaqueAction2> // will not work
export const DemoComponent = () => {
const dispatch = React.useReducer(() => null, null)[1]
const d0 : Dispatch = dispatch
const d1 : OpaqueDispatch1 = d0 // ok
const d2 : OpaqueDispatch2 = d0 // type error
}
The error is the following:
TS2322: Type 'Dispatch<Action>' is not assignable to type 'Dispatch<OpaqueAction2>'.
Type 'OpaqueAction2' is not assignable to type 'Action'.
Type 'OpaqueAction2' is not assignable to type '{ name: "toggleColumn"; }'.
Types of property 'name' are incompatible.
Type 'string' is not assignable to type '"toggleColumn"'.
^ but in the code above we actually assign "toggleColumn"
to string
. Something is wrong.
Here is the sandbox: https://codesandbox.io/s/crazy-butterfly-yldoq?file=/src/App.tsx:504-544
Upvotes: 0
Views: 472
Reputation: 51886
You're not assigning "toggleColumn"
to string
, you're assigning Dispatch<Action>
to Dispatch<OpaqueAction2>
.
The problem is that Dispatch<Action>
is a function that can only handle a parameter with a name
property "toggleColumn"
, while Dispatch<OpaqueAction2>
is a function that can handle a parameter with a name
property of any string
type. The assignment implies that Dispatch<Action>
should be able to handle any string
type as well, but it can't.
A function (...args: T) => R
is assignable to (...args: U) => R
if and only if U
is assignable to T
. This is why the first two lines of your error message reverse the order of the types:
Type 'Dispatch<Action>' is not assignable to type 'Dispatch<OpaqueAction2>'.
Type 'OpaqueAction2' is not assignable to type 'Action'.
Upvotes: 2