-
-
Notifications
You must be signed in to change notification settings - Fork 12
Typestates? #27
Comments
Thanks @bard! Yes, we don't currently support this but we should. It will mean some added complexity but it's worth it to support this pattern. @davidkpiano do you plan to support Typestates in V5? |
@bard There are several improvements we could bring here. First, as you say, this pattern: type TypeState = { value: 'green'; context: 'green-context' } | { value: 'red'; context: 'red-context' };
if (state.matches('green')) {
// This should error, because we now know that context === 'green-context'
state.context === 'red-context';
} Also, in options passed to interpret, useMachine or the second argument of |
Indeed, now that you mention it the second case is something I found myself wishing as well. I did work around the lack of it by e.g. casting event types, but I can easily see myself lose track of things in more complex machines and cast to the wrong type. |
Definitely - the casting of event types is something we've hopefully solved already. We should aim to do no casting whatsoever inside any options passed |
@bard I don't use Typestates day-to-day so I'll have some questions about usage. Can you model nested states with Typestates? Or only the top-level? For instance, could you model this: type State = { value: 'foo'; context: false } | { value: 'foo.child'; context: true }; |
I haven't dealt with that directly (I've used nested machines rather than nested states), but according to the Typestate section in the docs:
XState seems to use the |
Yes, I think it's falling out of favour. Thanks for your help 👍 |
Yeah, V5 will enforce using objects instead, e.g., However, Typestates (at least right now) should manually be specified, since they can't be easily inferred from the machine. |
Yep, inferring Typestates is beyond our power but we can certainly support them. |
Got a POC working here: This should work with the current Typestate API. It doesn't yet handle the |
const service = interpret(appMachine).start()
if (service.state.matches('idle')) {
console.log(state)
/*
(property) Interpreter<AppContext, any, AppEvent, AppState>.state: State<AppContext, AppEvent, any, AppState> & State<{
bar: string;
}, AppEvent, any, AppState> & {
value: "idle";
}
*/
}
export const App = () => {
const [state] = useMachine(appMachine)
if (state.matches('idle')) {
console.log(state)
/*
const state: State<AppContext, AppEvent, any, {
value: any;
context: AppContext;
}> & {
value: string;
}
*/
} |
Interesting - this might be worth an issue on the xstate repo if there isn't one already. Thanks, this gives me some more to go on. |
Are you using |
@davidkpiano no. I've reproduced it in this sandbox , forked from codesandbox.io's React+TypeScript sandbox — there's actually a typing issue even before any attempt at type narrowing — but the same works in this sandbox forked from XState's official React+TypeScript sandbox, including type narrowing. Not sure what's going on. |
@davidkpiano oh, wait, is 1.0.0-rc.4 @next? If so, then it works with @next. |
@bard Added inferencing on Any chance you could check it out locally? You can run |
@mattpocock. Wow, that was fast. Going to try it soon! |
I haven't been able to break it so far. Context is narrowed correctly and plenty other things that would have caused a runtime error fail early with a sweet type error (but you already knew that). I'm going to try and sketch a simple app to see how it fares. Something (unrelated) I noticed is that type information in the editor occasionally gets out of sync, and e.g. |
@bard That 'any' thing is to do with the test suite - it rimraf's the entire node_modules dir, npm installs it again and rebuilds the types from scratch. This shouldn't happen during watching. Thanks for testing this out, much appreciated. Do you need this pushed to a next branch so you can install it easier? |
One thing to test actually - are you getting type checking when you use |
I see, makes sense!
I can keep using the branch, no problem.
Yes, on this: const appMachine = createMachine<Context, Event, State, 'app'>({
// ...
states: {
idle: {
// ...
},
playing: {
// ...
},
},
});
const service = interpret(appMachine, { /* ... */ });
service.start();
service.state.matches('dummy'); I get this:
|
Alright, great. I'll close this issue and look to roll this out this week. |
@bard I'm actually going to re-open this. I think we can do better. We may even be able to infer typestates without you having to provide them. |
Hmm, wondering how that would look like. What would you infer them from? |
Let's imagine that a user has declared all their type Context = { hasBeenChanged: 'no' | 'yes' };
const machine = Machine<Context>(
{
initial: 'red',
context: {
hasBeenChanged: 'no',
},
states: {
red: {
after: {
2000: { actions: ['changeContext'], target: 'green' },
},
},
green: {},
},
},
{
actions: {
changeContext: assign(() => {
return { hasBeenChanged: 'yes' };
}),
},
},
); If we generate a TS graph of the config, we can introspect what each
This only works, though, if you declare your assign actions in the second parameter of your |
Two thoughts:
{
actions: {
changeContext: assign((context, event) => {
return { ...context, counter: context.counter + event.incrementBy };
}),
},
}, I'd think that the type of
|
Would we be able to provide (or merge in) external typestates in cases where we wanted to provide actions outside of the machine declaration (for example, in the second parameter of |
@Andarist could I get your opinion on this thread? I'm swaying towards the idea that we should support typestates passed in as a generic from the user, but also look to infer them for other users down the line. |
I think that we shouldn't support custom typestates because it's an unsafe feature and the whole goal of this project is to provide type safety. Or rather - we should not allow them unless one makes a compelling case that they are way better over what we can provide out of the box. That being said - computing typestates won't happen over night here so as a stopgap we can allow them for the time being but with a plan to remove the support for them when we reach the point of being able to compute them ourselves. |
Hello everyone.I was come across with this problem.But i very want use typescript on 100% .I created small solution,this may look like crutch,but it is very type save |
I don't think this can ever be truly type-safe without a compiler backing this up. It seems nearly impossible to validate at type-level that in a certain state you 100% deal with a particular typestate. To do that you need to analyze the graph. |
Yes you are right. But my solution might help with this. If you use my utility functions, you will get restrictions. Typescript won't give you the wrong context or invalid event if you're honest) |
@mattpocock Does #28 bring typestates when you explicitly define them like described in the xstate docs? Sry, I couldn't figure out from this discussion what changes it makes.
If so it would be nice if #28 could be merged, since the typestates are helpful even if you need to define them explicitly. |
First: thanks for bringing more safety to the machines!
I was wondering whether support is planned for Typestates, to narrow context type inside statements such as
if (state.matches('foo')) { ... }
?The text was updated successfully, but these errors were encountered: