Visitor Pattern in TypeScript

André Michelle avatar

André Michelle

The Visitor Pattern is an effective strategy in object-oriented design for handling multiple known types, eliminating the need for repetitive instanceof checks. This pattern allows each type to be processed in a specific context, aligning with the Open/Closed Principle for better extensibility and code maintainability.

A key feature in this approach is the safeExecute method, which enhances the flexibility of the Visitor Pattern. safeExecute is a higher-order function that safely invokes methods only if they are defined, allowing for partial implementation of visitors without risking runtime errors. This is achieved using conditional invocation in TypeScript, ensuring type safety and robustness in handling diverse object types.

type AnyFunc = (...args: any[]) => any
type Nullish<T> = T | undefined | null

const safeExecute = <F extends AnyFunc>(func: Nullish<F>, ...args: Parameters<F>): Nullish<ReturnType<F>> => func?.apply(null, args)

class Foo implements Visitable {
    accept<RETURN>(visitor: Visitor<RETURN>): Nullish<RETURN> {
        return safeExecute(visitor.visitFoo, this)
    }

    foo(): string {return "foo"}
}

class Bar implements Visitable {
    accept<RETURN>(visitor: Visitor<RETURN>): Nullish<RETURN> {
        return safeExecute(visitor.visitBar, this)
    }

    bar(): number {return 42}
}

class Baz implements Visitable {
    accept<RETURN>(visitor: Visitor<RETURN>): Nullish<RETURN> {
        return safeExecute(visitor.visitBaz, this)
    }

    baz(): string {return "303"}
}

interface Visitor<RETURN = void> {
    visitFoo?(foo: Foo): RETURN
    visitBar?(bar: Bar): RETURN
    visitBaz?(baz: Baz): RETURN
}

interface Visitable {
    accept<RETURN>(visitor: Visitor<RETURN>): Nullish<RETURN>
}

// This approach includes repetitive instanceof checks and branches
const a: Array<Nullish<string>> = [new Foo(), new Bar(), new Baz()]
    .map(x => {
        switch (true) {
            case x instanceof Foo:
                return x.foo()
            case x instanceof Bar:
                return x.bar().toString(10)
	        // You do not have to implement all cases
        }
    })
    
// The Visitor Pattern reads more descriptive
const b: Array<Nullish<string>> = [new Foo(), new Bar(), new Baz()]
    .map((x: Visitable) => x.accept({
        visitBar: (bar: Bar): string => bar.bar().toString(10),
        visitFoo: (foo: Foo): string => foo.foo()
        // You do not have to implement all cases either
    }))

// The result is the same
console.log(a) // ["42", "foo", undefined]
console.log(b) // ["42", "foo", undefined]
André Michelle avatar
Written By

André Michelle

Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.