Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 🏷️ stronger types for StateValue when using Typegen #3342

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ import { IS_PRODUCTION } from './environment';
import { TypegenDisabled, TypegenEnabled } from './typegenTypes';
import { BaseActionObject, Prop } from './types';

export function stateValuesEqual(
a: StateValue | undefined,
b: StateValue | undefined
export function stateValuesEqual<TResolvedTypesMeta = TypegenDisabled>(
a: StateValue<TResolvedTypesMeta> | undefined,
b: StateValue<TResolvedTypesMeta> | undefined
): boolean {
if (a === b) {
return true;
Expand Down Expand Up @@ -92,7 +92,7 @@ export class State<
TTypestate extends Typestate<TContext> = { value: any; context: TContext },
TResolvedTypesMeta = TypegenDisabled
> {
public value: StateValue;
public value: StateValue<TResolvedTypesMeta>;
public context: TContext;
public historyValue?: HistoryValue | undefined;
public history?: State<
Expand Down Expand Up @@ -253,7 +253,7 @@ export class State<
* @param events Internal event queue. Should be empty with run-to-completion semantics.
* @param configuration
*/
constructor(config: StateConfig<TContext, TEvent>) {
constructor(config: StateConfig<TContext, TEvent, TResolvedTypesMeta>) {
this.value = config.value;
this.context = config.context;
this._event = config._event;
Expand Down Expand Up @@ -288,10 +288,7 @@ export class State<
* @param stateValue
* @param delimiter The character(s) that separate each subpath in the string state node path.
*/
public toStrings(
stateValue: StateValue = this.value,
delimiter: string = '.'
): string[] {
public toStrings(stateValue = this.value, delimiter: string = '.'): string[] {
if (isString(stateValue)) {
return [stateValue];
}
Expand Down Expand Up @@ -338,8 +335,8 @@ export class State<
TTypestate,
TResolvedTypesMeta
> & { value: TSV };
public matches(parentStateValue: StateValue): any {
return matchesState(parentStateValue as StateValue, this.value);
public matches(parentStateValue: StateValue<TResolvedTypesMeta>): any {
return matchesState(parentStateValue, this.value);
}

/**
Expand Down
66 changes: 43 additions & 23 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@ class StateNode<
*/
public getStateNodes(
state:
| StateValue
| StateValue<TResolvedTypesMeta>
| State<TContext, TEvent, any, TTypestate, TResolvedTypesMeta>
): Array<
StateNode<
Expand All @@ -702,7 +702,9 @@ class StateNode<
const initialStateValue = this.getStateNode(stateValue).initial;

return initialStateValue !== undefined
? this.getStateNodes({ [stateValue]: initialStateValue } as StateValue)
? this.getStateNodes({
[stateValue]: initialStateValue
} as StateValue<TResolvedTypesMeta>)
: [this, this.states[stateValue]];
}

Expand Down Expand Up @@ -758,7 +760,7 @@ class StateNode<
);
return new State({
...stateFromConfig,
value: this.resolve(stateFromConfig.value),
value: this.resolve<TResolvedTypesMeta>(stateFromConfig.value),
configuration,
done: isInFinalState(configuration, this),
tags: getTagsFromConfiguration(configuration),
Expand All @@ -780,8 +782,8 @@ class StateNode<

return next;
}
private transitionCompoundNode(
stateValue: StateValueMap,
private transitionCompoundNode<TResolvedTypesMeta>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the TResolvedTypesMeta "flow" here from the outer type params? Right now I think that TResolvedTypesMeta should be the same across this whole class - but in here, we allow it to be different

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're 100% correct - great catch! should be fixed here: b2a466f

stateValue: StateValue<TResolvedTypesMeta>,
state: State<TContext, TEvent>,
_event: SCXML.Event<TEvent>
): StateTransition<TContext, TEvent> | undefined {
Expand All @@ -800,8 +802,8 @@ class StateNode<

return next;
}
private transitionParallelNode(
stateValue: StateValueMap,
private transitionParallelNode<TResolvedTypesMeta>(
stateValue: StateValue<TResolvedTypesMeta>,
state: State<TContext, TEvent>,
_event: SCXML.Event<TEvent>
): StateTransition<TContext, TEvent> | undefined {
Expand Down Expand Up @@ -854,8 +856,8 @@ class StateNode<
)
};
}
private _transition(
stateValue: StateValue,
private _transition<TResolvedTypesMeta>(
stateValue: StateValue<TResolvedTypesMeta>,
state: State<TContext, TEvent, any, any, any>,
_event: SCXML.Event<TEvent>
): StateTransition<TContext, TEvent> | undefined {
Expand Down Expand Up @@ -1521,23 +1523,31 @@ class StateNode<
*
* @param stateValue The partial state value to resolve.
*/
public resolve(stateValue: StateValue): StateValue {
public resolve<TResolvedTypesMeta>(
stateValue: StateValue<TResolvedTypesMeta>
): StateValue<TResolvedTypesMeta> {
if (!stateValue) {
return this.initialStateValue || EMPTY_OBJECT; // TODO: type-specific properties
return (
(this.initialStateValue as StateValue<TResolvedTypesMeta>) ||
(EMPTY_OBJECT as StateValue<TResolvedTypesMeta>)
); // TODO: type-specific properties
}

switch (this.type) {
case 'parallel':
return mapValues(
this.initialStateValue as Record<string, StateValue>,
this.initialStateValue as Record<
string,
StateValue<TResolvedTypesMeta>
>,
(subStateValue, subStateKey) => {
return subStateValue
? this.getStateNode(subStateKey).resolve(
stateValue[subStateKey] || subStateValue
)
: EMPTY_OBJECT;
}
);
) as StateValue<TResolvedTypesMeta>;

case 'compound':
if (isString(stateValue)) {
Expand All @@ -1547,23 +1557,31 @@ class StateNode<
subStateNode.type === 'parallel' ||
subStateNode.type === 'compound'
) {
return { [stateValue]: subStateNode.initialStateValue! };
return {
[stateValue]: subStateNode.initialStateValue!
} as StateValue<TResolvedTypesMeta>;
}

return stateValue;
}
if (!Object.keys(stateValue).length) {
return this.initialStateValue || {};
return (this.initialStateValue ||
{}) as StateValue<TResolvedTypesMeta>;
}

return mapValues(stateValue, (subStateValue, subStateKey) => {
return subStateValue
? this.getStateNode(subStateKey as string).resolve(subStateValue)
: EMPTY_OBJECT;
});
return mapValues(
stateValue as Record<string, unknown>,
(subStateValue, subStateKey) => {
return subStateValue
? this.getStateNode(subStateKey as string).resolve(
subStateValue as string | StateValueMap
)
: EMPTY_OBJECT;
}
) as StateValue<TResolvedTypesMeta>;

default:
return stateValue || EMPTY_OBJECT;
return (stateValue || EMPTY_OBJECT) as StateValue<TResolvedTypesMeta>;
}
}

Expand Down Expand Up @@ -1618,7 +1636,7 @@ class StateNode<
}

public getInitialState(
stateValue: StateValue,
stateValue: StateValue<TResolvedTypesMeta>,
context?: TContext
): State<TContext, TEvent, TStateSchema, TTypestate, TResolvedTypesMeta> {
this._init(); // TODO: this should be in the constructor (see note in constructor)
Expand Down Expand Up @@ -1658,7 +1676,9 @@ class StateNode<
);
}

return this.getInitialState(initialStateValue);
return this.getInitialState(
initialStateValue as StateValue<TResolvedTypesMeta>
);
}

/**
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,11 @@ export interface StateValueMap {
* - For a child atomic state node, this is a string, e.g., `"pending"`.
* - For complex state nodes, this is an object, e.g., `{ success: "someChildState" }`.
*/
export type StateValue = string | StateValueMap;

export type StateValue<
TResolvedTypesMeta = TypegenDisabled
> = TResolvedTypesMeta extends TypegenEnabled
? Prop<Prop<TResolvedTypesMeta, 'resolved'>, 'matchesStates'>
: string | StateValueMap;
export interface HistoryValue {
states: Record<string, HistoryValue | undefined>;
current: StateValue | undefined;
Expand Down Expand Up @@ -1491,8 +1494,12 @@ export interface StateLike<TContext> {
_event: SCXML.Event<EventObject>;
}

export interface StateConfig<TContext, TEvent extends EventObject> {
value: StateValue;
export interface StateConfig<
TContext,
TEvent extends EventObject,
TResolvedTypesMeta = TypegenDisabled
> {
value: StateValue<TResolvedTypesMeta>;
context: TContext;
_event: SCXML.Event<TEvent>;
_sessionid: string | null;
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ import { StateNode } from './StateNode';
import { State } from './State';
import { Actor } from './Actor';
import { AnyStateMachine } from '.';
import { TypegenDisabled } from './typegenTypes';

export function keys<T extends object>(value: T): Array<keyof T & string> {
return Object.keys(value) as Array<keyof T & string>;
}

export function matchesState(
parentStateId: StateValue,
childStateId: StateValue,
export function matchesState<TResolvedTypesMeta = TypegenDisabled>(
parentStateId: StateValue<TResolvedTypesMeta>,
childStateId: StateValue<TResolvedTypesMeta>,
delimiter: string = STATE_DELIMITER
): boolean {
const parentStateValue = toStateValue(parentStateId, delimiter);
Expand Down Expand Up @@ -122,8 +123,8 @@ export function isStateLike(state: any): state is StateLike<any> {
);
}

export function toStateValue(
stateValue: StateLike<any> | StateValue | string[],
export function toStateValue<TResolvedTypesMeta = TypegenDisabled>(
stateValue: StateLike<any> | StateValue<TResolvedTypesMeta> | string[],
delimiter: string
): StateValue {
if (isStateLike(stateValue)) {
Expand Down