I am building a login machine with TypeScript
and xState
combined with React
UI.
My machine is:
import { LoginResponse } from 'async/authentication/responseModel';
import Data from 'data';
import { AUTH_TOKEN } from 'Machines/authentication/constants';
import services from 'services';
import { assign, DoneInvokeEvent, Interpreter, Machine, MachineConfig, State } from 'xstate';
export type AutomataContext = {
authentication: {
oldToken: {
exists: boolean;
token: string;
};
token: string;
};
login: {
email: string;
password: string;
};
};
export interface AutomataStatesSchema {
states: {
SEARCHING_EXISTING_AUTH_TOKEN: {};
VERIFYING_EXISTING_TOKEN: {};
LOGIN_VIEW: {};
AUTHENTICATING: {};
AUTHENTICATED: {};
FAILED_AUTH: {};
};
}
export enum Events {
ENTER_DATA = 'ENTER_DATA',
SUBMIT_LOGIN_DATA = 'SUBMIT_LOGIN_DATA',
}
export type SubmitLoginData = {
type: Events.SUBMIT_LOGIN_DATA;
};
export type EnterData = {
type: Events.ENTER_DATA;
value: string;
field: keyof AutomataContext['login'];
};
export type AutomataEvent = SubmitLoginData | EnterData;
export type AutomataService = Interpreter<AutomataContext, AutomataStatesSchema, AutomataEvent>;
export const enterLoginData = (context: AutomataContext, event: EnterData) => {
assign({
login: Object.assign(context.login, { [event.field]: event.value }),
});
};
export const authenticateUser = (context: AutomataContext, _: any) =>
services.authenticationService.login(context.login.email, context.login.password);
export const verifyToken = (context: AutomataContext, _: any) =>
services.authenticationService.validateToken(context.authentication.oldToken.token);
const loginMachineConfig: MachineConfig<AutomataContext, AutomataStatesSchema, AutomataEvent> = {
id: 'auth-machine',
initial: 'SEARCHING_EXISTING_AUTH_TOKEN',
context: {
authentication: {
oldToken: {
exists: false,
token: '',
},
token: '',
},
login: {
email: '',
password: '',
},
},
on: {
ENTER_DATA: {
actions: ['enterLoginData'],
},
SUBMIT_LOGIN_DATA: {
target: 'AUTHENTICATING',
},
},
states: {
SEARCHING_EXISTING_AUTH_TOKEN: {
entry: [
// getTokenFromLocalStorage
(context, event) => {
const authentication = Object.assign({}, context.authentication, {
oldToken: {
exists: true,
token: localStorage.getItem(AUTH_TOKEN),
},
});
assign({
authentication: authentication,
});
},
],
after: {
500: [
{
cond: 'doesOldTokenExist',
target: 'VERIFYING_EXISTING_TOKEN',
},
{
target: 'LOGIN_VIEW',
},
],
},
on: {
ENTER_DATA: undefined,
SUBMIT_LOGIN_DATA: undefined,
},
},
VERIFYING_EXISTING_TOKEN: {
invoke: {
src: 'verifyToken',
onDone: {
target: 'AUTHENTICATED',
actions: [
// assignExistingTokenToContext
(context, even) => {
assign({
authentication: Object.assign(context.authentication, {
token: context.authentication.oldToken.token,
}),
});
},
//sendLoginDataToStore
(context, event) => {
const payload = Data.creators.authentication.saveLoginDataToStore(context.authentication.token);
Data.store.dispatch(payload);
},
],
},
onError: {
target: 'LOGIN_VIEW',
actions: [
// removeExpiredTokenFromStorage
(context: AutomataContext, event: any) => localStorage.removeItem(AUTH_TOKEN),
],
},
},
on: {
ENTER_DATA: undefined,
SUBMIT_LOGIN_DATA: undefined,
},
},
LOGIN_VIEW: {},
AUTHENTICATING: {
invoke: {
src: 'authenticateUser',
onDone: {
target: 'AUTHENTICATED',
actions: [
// clearLoginData
() =>
assign({
login: (context: AutomataContext, event): any =>
Object.assign(context.login, { password: '', email: '' }),
}),
// assignAuthTokenToContext
(context: AutomataContext, event: DoneInvokeEvent<LoginResponse>) => {
assign({
authentication: Object.assign(context.authentication, {
token: event.data.token,
}),
});
},
// placeAuthTokenToLocalStorage
(context: AutomataContext, event: any) => localStorage.setItem(AUTH_TOKEN, context.authentication.token),
],
},
},
on: {
ENTER_DATA: undefined,
SUBMIT_LOGIN_DATA: undefined,
},
},
AUTHENTICATED: {
on: {
ENTER_DATA: undefined,
SUBMIT_LOGIN_DATA: undefined,
},
},
FAILED_AUTH: {},
},
};
const options: any = {
services: {
verifyToken,
authenticateUser,
},
actions: {
enterLoginData,
},
guards: {
doesOldTokenExist: (context: AutomataContext, _: any) => context.authentication.oldToken.exists === true,
},
};
export default Machine<AutomataContext, AutomataStatesSchema, AutomataEvent>(loginMachineConfig, options);
In my React component:
index.tsx
import { useInterpret, useMachine } from '@xstate/react';
import machine from 'Machines/authentication/login';
import Form from 'views/routes/authentication/Login/Form';
const Login = () => {
const loginService = useInterpret(machine);
return (
<>
<Form service={loginService} />
</>
);
};
export default Login;
Form.tsx
import { AutomataService } from 'Machines/authentication/login';
import { Link } from 'react-router-dom';
type Props = {
service: AutomataService;
};
const Form = ({ service }: Props) => {
//some react code here
}
I am getting a type mismatch error here
Type 'Interpreter<AutomataContext, AutomataStatesSchema, AutomataEvent, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>' is not assignable to type 'AutomataService'.
The types of 'machine.getStateNodes' are incompatible between these types.
Type '(state: StateValue | State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>) => StateNode<...>[]' is not assignable to type '(state: StateValue | State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>) => StateNode<...>[]'.
Types of parameters 'state' and 'state' are incompatible.
Type 'StateValue | State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>' is not assignable to type 'StateValue | State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>'.
Type 'State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>' is not assignable to type 'StateValue | State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>'.
Type 'State<AutomataContext, AutomataEvent, any, { value: any; context: AutomataContext; }, TypegenDisabled>' is not assignable to type 'State<AutomataContext, AutomataEvent, any, any, TypegenDisabled & { missingImplementations: { actions: never; delays: never; guards: never; services: never; }; } & AllowAllEvents & { ...; }>'.
Types of property 'machine' are incompatible.
Type 'StateMachine<AutomataContext, any, AutomataEvent, { value: any; context: AutomataContext; }, BaseActionObject, any, TypegenDisabled> | undefined' is not assignable to type 'StateMachine<AutomataContext, any, AutomataEvent, any, BaseActionObject, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }> | undefined'.
Type 'StateMachine<AutomataContext, any, AutomataEvent, { value: any; context: AutomataContext; }, BaseActionObject, any, TypegenDisabled>' is not assignable to type 'StateMachine<AutomataContext, any, AutomataEvent, any, BaseActionObject, any, TypegenDisabled & { missingImplementations: { ...; }; } & AllowAllEvents & { ...; }>'.
Types of property '__TResolvedTypesMeta' are incompatible.
Type 'TypegenDisabled' is not assignable to type 'TypegenDisabled & { missingImplementations: { actions: never; delays: never; guards: never; services: never; }; } & AllowAllEvents & { indexedActions: IndexByType<...>; indexedEvents: Record<...> & { ...; }; invokeSrcNameMap: Record<...>; }'
I am lost here, what this TypeScript
error is really trying to say. Is there something wrong with the TypeSystem itself? or Have I built the interpreter
wrong?
The way I've always done it is
InterpreterFrom<typeof myMachine>
. There are also type issues with the Machine constructor, I think. The official recommended way iscreateMachine()
.