0

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:

  1. Accept (multiple) generics
  2. Define wider types

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

1
  • 1
    When you are dealing with a function like dispatch, one which accepts a wider set of argument types extends one which accepts a narrower set. Commented Feb 5, 2021 at 7:54

1 Answer 1

2

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'.
Sign up to request clarification or add additional context in comments.

1 Comment

Yes, good catch. Anyone interested in details should search for "functions contravarience".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.