javascript - Hur man gör en elegant tillståndsövergångskarta i Typescript?


Om du kan komma på en bättre titel, var vänlig redigera!

Vad jag har är två enums, ungefär så här:

enum State {
    A = "A",
    B = "B",
    C = "C"
}

enum Event {
    X = "X",
    Y = "Y",
    Z = "Z"
}

Och jag vill deklarera en karta som säger "Om det aktuella tillståndet är S och händelsen är E så är det nya tillståndet S1".

Inte varje kombination av tillstånd och händelse kommer att ge ett nytt tillstånd, ibland gör händelsen ingenting.

Så, i pseudo-javascript vill jag skriva något som:

const transitions = {
    State.A: {
        Event.X: State.B,
        Event.Y: State.C
    },
    State.B: {
        Event.X: State.A
        Event.Z: State.C
    }
}

Tyvärr kan jag inte hitta ett sätt att skriva detta på ett elegant sätt. Objektliteraler kan inte ha icke-sträng-litteralnycklar (inte ens interpolerade) och indexertyper i Typescript kan inte vara unionstyper och… jag sitter fast. Allt jag kan komma på som fungerar är mycket mer omständligt och fult.

Svar #1

Ditt svar fungerar bra, men jag märkte att det finns ett litet problem med det.

Eftersom du använde ett generiskt string-index i StateMap innebär detta att du förlorar all typkontroll när du får åtkomst till StateMap.

Till exempel:

const stateTransitions: StateMap = {
    [State.A]: {
        [Event.B]: State.C
    }
};

const x = stateTransitions.whatever // x is { [index: string]: State }

Vi borde kunna veta att whatever aldrig kan existera på en StateMap.

Här är ett alternativ:

type StateMap = Record<State, Record<Event, State>>

Nackdelen med ovanstående är att det tvingar dig att skriva ut alla kombinationer av tillstånd/händelser för att kunna kompilera. Så du skulle behöva skriva något som:

const stateTransitions: StateMap = {
    [State.A]: {
        [Event.X]: State.C,
        [Event.Y]: State.C,
        [Event.Z]: State.C
    },

    [State.B]: {
        [Event.X]: State.C,
        [Event.Y]: State.C,
        [Event.Z]: State.C
    },

    [State.C]: {
        [Event.X]: State.C,
        [Event.Y]: State.C,
        [Event.Z]: State.C
    }
};

Det beror lite på hur du förväntar dig att implementera statskartan. Ska den inkludera varje stat som en övergripande nyckel? Om så är fallet är Record<State, ...> en bra utgångspunkt.

Ett av dina krav var:

Inte varje kombination av tillstånd och händelse kommer att ge ett nytt tillstånd, ibland gör händelsen ingenting.

Du kan täcka detta uttryckligen genom att ange att händelsen resulterar i samma tillstånd. Till exempel, om tillstånd A inte övergår när händelse B inträffar, då:

const stateTransitions: StateMap = {
  [State.A]: {
    [Event.B]: State.A
    // ...
  },
  // ...
}

Redigera: Följande utifrån kommentarerna nedan på detta svar, om du vill utesluta vissa stater från statskartan eftersom de till exempel är slutliga stater, kan du göra det på följande sätt:

type StatesToInclude = Exclude<State, State.C | State.B>

type StateMap = Record<StatesToInclude, Record<Event, State>>

// ...

Svar #2

Skitsamma, jag hittade svaret själv 3 minuter senare. :D Det finns "beräknade egenskapsnamn". Så...

type StateMap = {  [index: string]: { [index: string]: State } }

const stateTransitions: StateMap = {
    [State.A]: {
        [Event.B]: State.C
    }
};

Hakparenteserna är acceptabla och stör egentligen inte läsbarheten, tycker jag. Tack allihopa för att ni har rubberduckat! :)