Visitor Pattern in TypeScript
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
Clap to support the author, help others find it, and make your opinion count.