0

Suppose that I have the following design implemented in an OO language (e.g. Python). I know that the OO way is a bit unnatural and sometimes not preferred when using TypeScript. So what would be the functional (and idiomatic) way to implement the same design in TypeScript?

The ideas are:

  • I have a Car type to describe a generic car.
  • For all manual gearbox cars, I can provide an abstract class to:
    • implement the Car type following the classic steps to start a manual gearbox car, and
    • provide some hook abstract methods for subclass to implement, and
    • provide some methods with default impl so that subclasses can choose to reuse or override.
  • For other types of cars, I am free to implement the Car type in a different way.

This would be the Python code:

from abc import ABC, abstractmethod
import json

class Car(ABC):
    @abstractmethod
    def start(self):
        raise NotImplemented

class ManualGearboxCar(Car):
    def start(self):
        self.engage_clutch()
        self.start_engine()
        
        dashboard_info = self.read_dashboard()
        print("Dashboard:", json.dumps(dashboard_info))
        # ... Check dashboard_info and raise if anything goes wrong ...
        if not dashboard_info["ok"]:
            raise Exception("Something is not OK")

        self.shift_gear(1)
        self.release_clutch()
        print("Car started!")

    def read_dashboard(self) -> dict:
        return {
            "foo": "bar",
            "default": "values",
            "ok": True,
        }

    def engage_clutch(self):
        print("[DefaultClutch] engaged!")

    def release_clutch(self):
        print("[DefaultClutch] released!")

    @abstractmethod
    def start_engine(self):
        raise NotImplemented

    @abstractmethod
    def shift_gear(self, level: int):
        raise NotImplemented

class Picasso(ManualGearboxCar):
    def read_dashboard(self) -> dict:
        info = super().read_dashboard()
        return info | {
            "brand": "Citroen",
        }

    def start_engine(self):
        print("[Picasso] engine started!")

    def shift_gear(self, level: int):
        print("[Picasso] gear shifted to:", level)


class Tesla(Car):
    def start(self):
        print("[Tesla] Wow it just starts!")

if __name__ == "__main__":
    picasso = Picasso()
    picasso.start()

    tesla = Tesla()
    tesla.start()

This is what I come up with to implement the same thing in TypeScript, still in an OO style which is basically porting the same code from Python to TS:

type Car = {
  start(): void;
}

abstract class ManualGearCar implements Car {
  start(): void {
    this.engageClutch();
    this.startEngine();
    const dashboardInfo = this.readDashboard();
    // ... Check dashboard_info and raise if anything goes wrong ...
    console.log(`Dashboard: ${JSON.stringify(dashboardInfo)}`);
    if (!dashboardInfo.ok) {
      throw new Error("Something is not OK");
    }
    this.shiftGear(1);
    this.releaseClutch();
    console.log("Car started!");
  }

  readDashboard(): Record<string, any> & { ok: boolean } {
    return {
      foo: "bar",
      default: "values",
      ok: true,
    };
  }

  engageClutch(): void {
    console.log("[DefaultClutch] engaged!");
  }

  releaseClutch(): void {
    console.log("[DefaultClutch] released!");
  }

  abstract startEngine(): void;

  abstract shiftGear(level: number): void;
}


class Picasso extends ManualGearCar {
  readDashboard(): Record<string, any> & { ok: boolean; } {
    const info = super.readDashboard();
    return { ...info, brand: "Citroen" };
  }

  startEngine(): void {
    console.log("[Picasso] engine started!");
  }

  shiftGear(level: number): void {
    console.log(`[Picasso] gear shifted to: ${level}`);
  }
}

const createPicasso = (): Car => {
  return new Picasso();
}

const createTesla = (): Car => {
  return {
    start() {
      console.log("[Tesla] Wow it just starts!");
    }
  }
}

function main() {
  const picasso = createPicasso();
  picasso.start();

  const tesla = createTesla();
  tesla.start()
}

main();

What would you do in a functional style?

Disclaimer: I am not asking for a subjective judgement of a "better" functional solution. I'm asking for an objective answer of whether there also exist a functional solution to this concrete problem.

Thanks!

8
  • 1
    This seems fine to me. TS/JS supports both OOP and functional approaches to things, so the abstract class and factory functions can coexist. You could also make Tesla a regular class that implements Car. This feels like it might be an opinion question because it's hard to know who decides whether something is "idiomatic". Do you have some objective criterion by which we'd be able to verify an answer's correctness? Commented Apr 18, 2024 at 11:40
  • @jcalz Indeed, this would be an opinion question... By "idiomatic" I mean 1) this is the natural way a TS programmer would use (which is probably "opinionated", yes), and 2) it gives me full type safety of TS. For point 2, what bothers me a bit is that TS allows a subclass to specify a more narrow type for a parameter when overriding a method, which breaks the type safety. For point 1, to make it less opinionated, maybe the question actually is: Are there any other alternative but equally good ways to implement the same design? Commented Apr 18, 2024 at 11:56
  • So, unless you want this question to be closed as an opinion question, you might want to edit to make it ask a more objective version of it. Of course, "equally good" is also subjective unless you lay out what that means. If you're looking for complete type safety, TS doesn't really provide it, so at some point there's diminishing returns to refactoring to be safer. If you want to avoid method parameter narrowing you can refactor to use function-valued properties instead, but then your example should be a minimal reproducible example showing the problem directly so answers can target it. Commented Apr 18, 2024 at 12:04
  • Thanks for the suggestion. I edited the post to ask for a functional way. Commented Apr 18, 2024 at 13:53
  • Why does it need to be functional? It's better to explain why you need something instead of just asking for it. That helps you avoid the XY problem. Commented Apr 18, 2024 at 15:27

1 Answer 1

1

It looks like you're describing the Template Method pattern.

If you prefer a more functional style, as with many other design patterns, this can be easily implemented with a higher order function. You then pass functions as parameters in place of the abstract methods.

Your example is currently somewhat contrived, as the class has no state and can thus be trivially replaced with simple functions. Assuming you want state, you can replace the methods with functions that accept the state as a parameter and return the updated state. I've added an example below. In order to preserve the ability to extend the state in a "subclass" of ManualGearCar, I've made the create function (which replaces your constructor) generic.

namespace ManualGearCar {
   export interface Car {
    wheels: number;
    gears: number;
    currentGear: number;
    engineStarted: boolean;
   }
   
  function readDashboard(): Record<string, any> & { ok: boolean } {
    return {
      foo: "bar",
      default: "values",
      ok: true,
    };
  }

  function engageClutch(): void {
    console.log("[DefaultClutch] engaged!");
  }

  function releaseClutch(): void {
    console.log("[DefaultClutch] released!");
  }
   
   export function create<T extends Car>(state: T, startEngine: (state: T) => T, shiftGear: (state: T, gear: number) => T) : T {
    engageClutch();
    const state2 = startEngine(state);
    const dashboardInfo = readDashboard();
    // ... Check dashboard_info and raise if anything goes wrong ...
    console.log(`Dashboard: ${JSON.stringify(dashboardInfo)}`);
    if (!dashboardInfo.ok) {
      throw new Error("Something is not OK");
    }
    const state3 = shiftGear(state2, 1);
    releaseClutch();
    console.log("Car started!");
    return state3;
   } 
}


function createPicasso() {

  const state = {
    wheels: 4,
    gears: 5,
    currentGear: 0,
    engineStarted: false
  };  

  return ManualGearCar.create(
    state, 
    (s: ManualGearCar.Car) => ({...s, engineStarted: true}), 
    (s: ManualGearCar.Car, gear: number) => ({...s, gear: gear}));
}

interface RaceCar extends ManualGearCar.Car {
  maxSpeed: number
}

function createFormulaOneCar() {
  const state = {
    wheels: 4,
    gears: 5,
    currentGear: 0,
    engineStarted: false,
    maxSpeed: 400
  };  

  return ManualGearCar.create(
    state, 
    (s: RaceCar) => ({...s, engineStarted: true}), 
    (s: RaceCar, gear: number) => ({...s, gear: gear, maxSpeed: s.maxSpeed + 10}));
}
Sign up to request clarification or add additional context in comments.

1 Comment

Yes, it is the template method pattern. I found this for a TS impl, but I feel it is still very OO style. Using the functional style, I feel it would be difficult to reuse the methods defined in ManutalGearboxCar. Can you give some code snippet to better explain the idea? Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.