TypeScript event typing best practices #1591
-
Hi there 👋 I'm looking for some best practices for typing my events in TypeScript. Right now if I don't add the
I couldn't find anything in the docs or the examples that covered this. Are there best practices for typing your events or some examples that anyone can share that cover this? Thanks! Edit: here's a sandbox. import { memoize } from 'lodash';
import { assign, createMachine } from 'xstate';
import { UserClaims } from '@okta/okta-auth-js';
export const AUTHENTICATE = 'AUTHENTICATE';
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export type GlobalContext = {
claims: UserClaims | null;
error?: string;
};
export type GlobalEvent =
| { type: 'LOGOUT'; value: null }
| { type: 'AUTHENTICATE'; value: null }
| { type: 'LOGIN'; value: UserClaims };
export type GlobalState =
| {
value: 'authenticating';
context: GlobalContext & {
error: undefined;
};
}
| {
value: 'loggedIn';
context: GlobalContext & {
error: undefined;
};
}
| {
value: 'loggedOut';
context: GlobalContext & {
error: undefined;
};
};
export default memoize(function createGlobalMachine(initialState) {
return createMachine<GlobalContext, GlobalEvent, GlobalState>(
{
id: 'global',
initial: initialState,
context: {
claims: null,
error: undefined,
},
states: {
authenticating: {
on: {
LOGIN: {
target: 'loggedIn',
actions: 'setClaims',
},
},
},
loggedIn: {
on: {
LOGOUT: {
target: 'loggedOut',
actions: 'clearClaims',
},
},
},
loggedOut: {
on: {
AUTHENTICATE: {
target: 'authenticating',
},
},
},
},
},
{
actions: {
clearClaims: assign({
claims: (_ctx, _evt) => null,
}),
setClaims: assign({
claims: (_ctx, evt) => evt.value,
}),
},
}
);
}); |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 10 replies
-
✏️ Please create a CodeSandbox (XState TypeScript template) reproduction of this. Thanks! |
Beta Was this translation helpful? Give feedback.
-
Here's an idea: since any action in function assertEventType<TE extends EventObject, TType extends TE["type"]>(
event: TE,
eventType: TType
): asserts event is TE & { type: TType } {
if (event.type !== eventType) {
throw new Error(
`Invalid event: expected "${eventType}", got "${event.type}"`
);
}
} And that way, all you'll need to do is add that function: setClaims: assign({
claims: (_ctx, evt) => {
+ assertEventType(evt, "LOGIN");
return evt.value;
}
}) And it will be typed correctly! Check it out: https://codesandbox.io/s/xstate-typescript-template-forked-b19c4?file=/src/index.ts |
Beta Was this translation helpful? Give feedback.
-
For our own event types, we know what the type should be, so the above approach works. And could |
Beta Was this translation helpful? Give feedback.
-
And I have another question: export function isEventType<TE extends EventObject, TType extends TE["type"]>(
event: TE,
eventType: TType
): event is TE & { type: TType } {
return event.type === eventType;
} wo using the example above it can be a noop: setClaims: assign({
claims: (_ctx, evt) => isEventType(evt, "LOGIN") ? evt.value : _ctx.claims
}) (Does this make sense?) |
Beta Was this translation helpful? Give feedback.
Here's an idea: since any action in
actions
assume (rightfully) that any event can be sent to it, you have to explicitly guard against unwanted events. One way to do this with TypeScript is to use an assertion function:And that way, all you'll need to do is add that function:
setClaims: assign({ claims: (_ctx, evt) => { + assertEventType(evt, "LOGIN"); return evt.val…