Xstate: @xstate/react: typescript: state is always `never` after calling `.matches()` twice in the same expression

Created on 16 Jan 2020  路  12Comments  路  Source: davidkpiano/xstate

Description
@xstate/react: typescript: state is always never after calling .matches() twice (in the same expression?)

function App () {
  const [state, _] = useMachine(toggleMachine)

  if (state.matches('a') || state.matches('b')) {
    //                        ^ is of type `never`
    console.log('do a thing')
  }


  /*
   * doesn't work like this either
   *
   * if (state.matches('a')) {
   *   // ...
   * } else if (state.matches('b')) {
   *   // ...
   * }
   */
}

Is this an antipattern or am I doing something wrong?

Sadly I don't have enough typescript experience at the moment to understand the following code in detail:
https://github.com/davidkpiano/xstate/blob/d6eda85b28759b4cc296ca8f88633f2342045e31/packages/core/src/State.ts#L284-L291

Expected Result
I should be able to check if a state is either one of a state

Actual Result
I can't check if a type is one of either states

Reproduction

Simple Reproduction: https://codesandbox.io/s/xenodochial-faraday-05nmv

Additional context

{
  "@xstate/react": "^0.8.1",
  "xstate": "^4.7.6"
}
bug

Most helpful comment

Still an issue for me with latest everything: typescript 3.8.3, xstate 4.9.1, @xstate/react 0.8.1... in my case tsc is happy for some reason, it's only tsserver complaining. The <any> (or <unknown>) workaround doesn't help unfortunately.

EDIT: Found a working workaround,

const aState = state.matches("a");
if (aState) { return <A />; }

const bState = state.matches("b");
if (bState) { return <B />; }

... works when this doesn't:

if (state.matches("a")) { return <A />; }
if (state.matches("b")) { return <B />; }

All 12 comments

Workaround: state.matches<any>(...)

Workaround: state.matches<any>(...)

Just wanted to call out that this work around doesn't solve the TypeScript error for me in the aforelinked Code Sandbox.

Regardless, thank you for this great library and constant support.

P.S. sign up as a Brave Content Creator to be able to receive BAT tips 馃挭

I wrote myself this little helper to work around this issue:

export function matchesAny(state: State<Context, Event, Schema>, ...values: string[]): boolean {
  return values.reduce<boolean>((a, s) => a || state.matches(s), false);
}

Hm, actually it could be simplified to this probably:

export function matchesAny(state: State<Context, Event, Schema>, ...values: string[]): boolean {
  return values.some(s => state.matches(s));
}

Make sure you're using the latest version of TypeScript as well. For anyone who is still seeing issues, what version of TS do you have?

Here is a codesandbox with a simple reproduction. Interestingly I seem to sometimes only get an IDE error and sometimes an error when Typescript is compiling as well.

Typescript 3.8.2
xstate 4.8.1
@xstate/react 0.8.1

export function matchesAny(state: State, ...values: string[]): boolean {
return values.some(s => state.matches(s));
}

Where do I import Context and Schema from?

@cschlittPHI Those are types you provide yourself.

I think you should be able to set unknown for the type parameters, as they are not depended on by the implementation.

Still an issue for me with latest everything: typescript 3.8.3, xstate 4.9.1, @xstate/react 0.8.1... in my case tsc is happy for some reason, it's only tsserver complaining. The <any> (or <unknown>) workaround doesn't help unfortunately.

EDIT: Found a working workaround,

const aState = state.matches("a");
if (aState) { return <A />; }

const bState = state.matches("b");
if (bState) { return <B />; }

... works when this doesn't:

if (state.matches("a")) { return <A />; }
if (state.matches("b")) { return <B />; }

I didn't realize this issue has been raised before. I experienced the same problem and reported it here https://github.com/davidkpiano/xstate/issues/1142.

@jscheid solution of moving state.matches into the variable works.

Another workaround is to reassign the state.

import { State } from 'xstate';
import { useMachine } from '@xstate/react';

const Comp = () => {
  let [state, send] = useMachine(myMachine);

  if (state.matches('maybeNo'){
    return;
  }

  state = state as State<MyContext, MyEvent>

  // continue to use state as it no longer has the "never" type
}

The solution I've came up to solve this problem is simply assign the initial state value to different constants and then use those constants

const [state, send] = useMachine(machine)
const states = {
  submited: state,
  submitting: state,
  idle: state,
  invalid: state,
}
if (states.submited.matches('submited')) {/* ... */}
if (state.idle.matches('idle') {/* ... */}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

bradwoods picture bradwoods  路  3Comments

jfun picture jfun  路  3Comments

bradwoods picture bradwoods  路  3Comments

hnordt picture hnordt  路  3Comments

3plusalpha picture 3plusalpha  路  3Comments