#Issue creating strong config type for nested state machine

18 messages · Page 1 of 1 (latest)

nimble cargo
#

I'm trying to create a strong type config which will be used to configure a state machine, where "initial" should be a key of the states property in the same node.

Something like the following:

type StateNode<T extends string = string> = {
  initial: keyof T;
  states: Record<T, Partial<StateNode>>;
};

function createConfig<T extends StateNode>(config: T): T {
  return config;
}

const config = createConfig({
  initial: 'alpha', // type should be 'alpha' | 'beta'
  states: {
    alpha: {
      initial: 'x', // type should be 'x' | 'y'
      states: {
        x: {},
        y: {
          initial: 'z', // type should be 'z'
          states: {
            z: {},
          },
        },
      },
    },
    beta: {},
  },
});

Does anyone know how this can be achieved?

subtle thunder
#

perhaps something like this:

hexed scarabBOT
#
mkantor#0

Preview:```ts
type StateNode<T extends string = string> = {
initial: NoInfer<T>
states: Record<T, Partial<StateNode>>
}

function createConfig<T extends string>(
config: StateNode<T>
): StateNode<T> {
return config
}

const config = createConfig({
initial: "alpha", // type should be 'alpha' | 'beta
...```

nimble cargo
#

That works for the root level initial, but deeper levels have type string | undefined

subtle thunder
#

hm, yeah they do from an outsider's perspective. they're still enforced to line up with the states during creation though

subtle thunder
#

this changes what outsiders see, but at the cost of making creation less safe (e.g. it doesn't enforce that initial is among the states):

hexed scarabBOT
#
mkantor#0

Preview:```ts
type StateNode<T extends string = string> = {
initial: NoInfer<T>
states: Record<T, unknown>
}

function createConfig<
T extends string,
U extends StateNode<T>

(config: U): U {
return config
}

const config = createConfig({
initial: "alpha", // type should be 'alpha' | 'be
...```

nimble cargo
warm ginkgo
#

You can use a validator type.

#

Ah, can remove the nested createConfigs too.

hexed scarabBOT
#
nonspicyburrito#0

Preview:```ts
type StateNode = {
initial: string
states: Record<string, object | StateNode>
}

type CoerceStateNode<T extends StateNode> = {
initial: keyof T["states"]
states: {
[K in keyof T["states"]]: T["states"][K] extends StateNode
? CoerceStateNode<T["stat
...```

warm ginkgo
#

Autocomplete within createConfig, type checking, and the final output should all work.

subtle thunder
#

nice. i don't think | StateNode is necessary/useful for states values though

warm ginkgo
#

Oh that's true.

#

Actually, it does help with providing autocomplete, eg when you are within beta: {}

subtle thunder
#

ah, sneaky

warm ginkgo
#

Tbf, it wasn't intended 😄

#

I would assume the actual code would be something more like SomeOtherIncompatibleNode | StateNode, because otherwise there's not really a way for you to tell at runtime if it's object or StateNode.