0

I am attempting to create a 'movable entity' that returns a new state whenever it is 'moved'. I am running into two errors I am unsure how to solve. The first is when I create a Movable from an Entity, I need to populate the Entity with a 'move' object. This object relies on the fact that the parent object will now have a 'move' object as a child. You can see the specific error at 1) (this still runs, I'd just rather have it be typed correctly).

For 2), I cannot call entity.move.nextState() and am unsure why. This is the main blocker. Any ideas?

Here is the code in a Typescript playground

(Skip down to testMove to get an idea of what the function is doing)

type Nullable<T> = T | null

interface IEntityState {
    id: string
}

interface IPosition {
    x: number, y: number, z: number
}


interface IComponent extends IEntityState { }

interface Actionable {
    nextState: (...args: any) => IEntityState
}

interface Undoable {
    initialState: Nullable<any>
    undo: () => IEntityState | Error // TODO Make an Either
    commit: () => IEntityState
}

/** Movable */

interface IMoveAction extends Actionable, Undoable {
    initialState: Nullable<any>
    nextState: (position: IPosition) => IMovable
    undo: () => IMovable | Error
    commit: () => IMovable
} 

interface IMovable extends IComponent {
    position: IPosition
    move: IMoveAction
}

const MoveAction = <O extends IMovable>(object: O): IMoveAction => 
({
    initialState: null,
    // Return with an initial move state if it does not exist already
    nextState: (position: IPosition): IMovable =>
        initialState
            ? { ...object, position }
            : { ...object, position, move: { ...object.move, initialState: position } }
    ,
    undo: () => 
        initialState
            ? { ...object, position: initialState, move: { ...object.move, initialState: null } }
            : new Error('Cannot undo a move with no initialState')
    ,
    commit: () => 
        ({ ...object, position: initialState, move: { ...object.move, initialState: null } })
    ,
})

type MovableInput = IEntityState & { position: IPosition }
const Movable = <O extends MovableInput>(object: O): O & IMovable =>
({
    ...object,
    move: MoveAction(object), // 1) Argument of type 'O' is not assignable to parameter of type 'IMovable'. Property 'move' is missing in type 'MovableInput' but required in type 'IMovable'
})

function testMove() {
    console.log('Running move component tests')

    const id = 'test'
    const initialPosition: IPosition = { x: 3, y: 2, z: 3 }
    const newPosition: IPosition     = { x: 3, y: 2, z: 0 }
    const entity: MovableInput = { id, position: initialPosition }
    const initialState = Movable(entity)

    // 2) Throws error 'Cannot access 'initialState' before initialization'
    const nextState = initialState.move.nextState(newPosition)
    const undoneState = nextState.move.undo() as IMovable

    // Initial state is preserved
    console.assert(initialPosition === nextState.move.initialState)

    // State transitions correctly
    console.assert(initialState.position !== nextState.position)

    // We can undo actions
    console.assert(nextState.position !== undoneState.position)
    console.assert(initialState.position === undoneState.position)

    // We cannot undo commited changes
    const committedState = nextState.move.commit()
    const error = committedState.move.undo() as Error
    console.assert(error.message === 'Cannot undo a move with no initialState')
} testMove()
user82395214
  • 829
  • 14
  • 37

1 Answers1

1

You try to use the object property initialState before the MoveAction object is created. A solution would be using getters, a far better solution rewriting the code and use classes instead of plain objects.

The getter approach would be like this:

const MoveAction = <O extends IMovable>(object: O): IMoveAction => 
({
    initialState: null,
    // Return with an initial move state if it does not exist already
    get nextState() { return function(position: IPosition): IMovable {
        return this.initialState
            ? { ...object, position }
            : { ...object, position, move: { ...object.move, initialState: position } }
            }
        },
    get undo() { return function() { 
        return this.initialState
            ? { ...object, position: this.initialState, move: { ...object.move, initialState: null } }
            : new Error('Cannot undo a move with no initialState')
        }
    },
    get commit() {return function() { 
        return ({ ...object, position: this.initialState, move: { ...object.move, initialState: null } })
    }},
})

You see, it's even more complicated/less understandable and now we have typescript warnings on the this. I really wouldn't go that way. A MoveAction class would be far easier to handle and to understand.

Apart from that: now there's another error on the next step in the test, but that seems to be a different story.

Reference: Self-references in object literals / initializers

Edit

The problem was, didn't see it in the beginning, that we need to access initialState with on this and this has to be the object itself. Therefor we need normal functions, arrow functions won't work. The following version does not show the reported error. The new error or undo is something different.

const MoveAction = <O extends IMovable>(object: O): IMoveAction => 
({
    initialState: null,
    // Return with an initial move state if it does not exist already
    nextState(position: IPosition): IMovable {
        return this.initialState
            ? { ...object, position }
            : { ...object, position, move: { ...object.move, initialState: position } }
    },
    undo(): IMovable | Error { 
        return this.initialState
            ? { ...object, position: this.initialState, move: { ...object.move, initialState: null } }
            : new Error('Cannot undo a move with no initialState')
    },
    commit(): IMovable{ 
        return { ...object, position: this.initialState, move: { ...object.move, initialState: null } }
    },
})
Andreas Dolk
  • 113,398
  • 19
  • 180
  • 268
  • In `const Movable` I am returning a new object that contains `initialState`, `nextState`, `undo`, and `commit`. How come this object isn't initialized? Also, I'm trying to avoid classes and stick to simple objects. – user82395214 May 25 '20 at 16:39
  • Then why using typescript which *is* about using classes? What's the reason for avoiding that, your code is really hard to understand. Would be far easier with classes. – Andreas Dolk May 25 '20 at 17:26
  • I honestly may being going about it the wrong way but I'm trying to make everything functional and therefore trying to avoid classes. Typescript is helpful because it provides type hints – user82395214 May 25 '20 at 23:54
  • 1
    Sorry, I was wrong and mislead by the referenced answer. Your approach works, you just have to be careful with 'this' and arrow functions. I've added a fixed `MoveAction` to the answer. – Andreas Dolk May 26 '20 at 06:06
  • So when `nextState` is called, why does the object lose it's undo property? – user82395214 May 26 '20 at 23:24
  • Because new the object that you return with `initialState.move.nextState(newPosition)` does not have an `undo` method. But you try to call `undo` on that. (Your code is really hard to understand. Ask yourself: would you be able to fix an issue or add a new feature in a year or so? If it's not a clear 'sure!', then consider making it maintainable. And classes would greatly help). – Andreas Dolk May 27 '20 at 06:14