Hacker News new | ask | show | jobs
by tunesmith 1373 days ago
Might as well ask here. On our teams, we have the occasional developer that is insistent on using Typescript in an OO fashion. This has always struck me as square peg round hole. Even though I come from an OO background, Typescript strict settings really seem to push me in a direction of using interfaces and types for type signatures, and almost never classes, subclasses, instantiated objects. I don't have a very good answer for "yeah, but what about dependency injection"? though. Any thoughts from anyone?
12 comments

>I don't have a very good answer for "yeah, but what about dependency injection"? though. Any thoughts from anyone?

There is no "dependency injection" in a functional world, take this opportunity to show your colleague how FP makes their life easier. It's just a function.

Instead of a class, implementing an interface, created by a factory, requiring a constructor, all you need is a function.

Anything that was previously a "dependency" in OO terms is now an argument to your function. If you want to "inject" that dependency you simply partially apply your function, the result is then of course a function with that "dependency" "injected" which can then be used as usual. In JavaScript there's even a nifty built-in prototype method on every function called `Function.prototype.bind` which allows you to do the partial application to create the "dependency injected" function!

Example:

```

const iRequireDependencies = (dependencyA, dependencyB, actualArgumentC, actualArgumentD, ...etc) => console.log(dependencyA, dependencyB, actualArgumentC, actualArgumentD, ...etc);

const withRandomDependencies = iRequireDependencies.bind(undefined, 'randomA', 'randomB')

withRandomDependencies('actualA', 'actualB', 'actualC', 'actualD', 'actualE') // etc

// => 'randomA' 'randomB' 'actualA' 'actualB' 'actualC' 'actualD' 'actualE'

```

I think the problem with DI is less about how they are passed in and more about how usually there is are only two implementations: the real one and a test one. The test one is a mock/stub based on assumed behaviours of the real thing. Obviously, such an approach is essential in some situations but in general it is mostly bad.
The problem with that is that eventually you want to have the dependencies automatically injected (like how an IoC container would be used in a typical OOP application).

Sure, there's solutions for this in the FP world, but in my experience they tend to have their own drawbacks. Admittedly, I've only ever used TS on the front-end (with no DI), so I've never really looked at what FP-style libraries exist for this.

You could model that with one function taking that dependency and returning a new one with it included. Then you just use the one that has it included instead of the one that doesn't.
You still have to construct and pass in that dependency though, along with all of its dependencies, recursively.
Are you seeing any decorator or OOP style in React? Yet plenty of dependencies are automatically injected without the OOP jargon, if you want to see a pure JS example of auto DI go check angular 1.5 dep system, and Vuejs.
> React

I totally agree there are other good/better options (as evidenced by react and many others), but I don't think this is an entirely fair comparison. The main "DI" alternatives in React are hooks, context, and imports, none of which could really replace traditional IoC containers on the backend without some modifications.

I think the main thing that makes automatic DI easy with OOP is the clear separation between dependencies (constructor parameters), and method parameters. Admittedly, this is totally possible with FP, but requires some good conventions and doesn't seem to be nearly as popular.

I also think most OOP langs have terrible syntax for constructors, which makes it look clunkier than it really is. Primary constructors (e.g. kotlin) make this not much more verbose than the FP alternative.

> Angular 1.5 dep system

Yes it's vanilla JS, but I doubt there's many FP people that would call that functional. The examples I saw are all just using JS functions as "discount classes", which definitely aren't pure or functional.

Why do you need automatic injection? What the parent said is idiomatic FP
Because in typical back-end software, the dependency tree can get very big very fast. Having an IoC container means devs only have to declare direct dependencies for each service, rather than constructing the entire tree (and figure out the necessary ordering, etc).
I get this question sometimes from a developer new to my team asking if it’s ok to add OOP code since most of the existing code is just functions.

My view on that is that it’s ok to use OOP and define classes if you are really defining an OOP style object. Back in the 90s is was taught that an object has identify, state and behaviour. So you you don’t have all three, it’s not really an object in the OOP style.

Looking at it through this lens helps make it clearer when you should add classes or just stick to function and closures.

I'll give a practical, non-philosophical answer.

Indeed, if you want to use emitDecoratorMetadata for automatic dependency injection, you should use classes. If the library itself takes advantage (again likely due to decorators) of classes e.g. https://typegraphql.com/docs/getting-started.html then yes, classes are again a fine choice.

The general answer is that they're useful when the type also needs to have a run-time representation (and metadata). Otherwise, not really.

I keep our stack mostly functional and that’s how it was done before me on our current project.

A few objects contain state like say a DB connection/client or a RequestContext you pass down through your request handler middleware’s. Those are an OOP class with an interface definition.

Everything else is just functions and closures. We also generate interface objects from our GraphQL types but that’s not a real OOP type, it’s just an interface.

If you keep to that structure, you’ll largely avoid the whole polymorphism OOP type hierarchy hell and all the dangers that come with it.

As for DI (dependency injection), that’s honestly just a fancy form of passing parameters down through function calls. Technically, the RequestContext I mentioned before is a “ball of mud” provider pattern DI code smell. So maybe down the road we will use DI to create more constrained context scopes.

If I do go that route for DI, I would likely strongly follow a CQRS style class pattern to inject objects and keep them nicely named and organized. Would also fit nicely pattern wise with the existing function + closures architecture.

But yeah, overall, stick to functions and closures, use OOP style classes sparingly and you’ll get the best of all worlds.

Either is valid.

If you got your first taste of typescript from angular and have a full stack background in c#/ Java class based style will make you feel right at home.

React seems to oscillate between the 2 styles.

My recent work in Svelte send to favor functions and types.

IMO the biggest benefit of classes is the code organization it brings. Have you ever seen a “util” class or folder. That’s what tends to happen to a code base without strong cohesion. It becomes hard to find anything.

With typescript/js you have modules as a pretty good substitute.

What I love about typescript is that you can mix the two.

Mostly classless module based with the occasional class (logger with a constructor to pass in the current module name for example) seems to be what I like most now. Use what makes sense.

OO programming is a very versatile paradigm, and not at all incompatible with JS/TS.

It comes down to choice; pick one for the project and be consistent. That’s all that matters.

DI does not require OOP, and you don’t need DI to write good TS/JS code. DI is common in OOP, but if you aren’t using OOP you don’t need DI anyway.

I successfully used functional programming with DI and it was quite pleasant!

Because DI is just “give me the dependencies I need when I declare I need it” you can use simple classes as scopes similar to CQRS patterns and continue doing functional programming from there.

It’s quite neat how you can interchange between the two and have it work rather nicely.

Technically, you could even do the same thing with closures and avoid OOP style classes all together even.

DI lives on, it just looks a little bit different than the constructor injection we’re used to seeing in OOP.

Do you mean something like this: f(arg1, arg2, arg3, deps)?
As I am not familiar with TS, I am gonna use F# as an example:

    let sort (iterator: 'b -> 'a list) (collector: 'a list -> 'c) (comparator: 'a -> 'a -> bool)  (collection: 'b) -> 'c =
        ...
I added the type annotations to, hopefully make it clear. The iterator is a helper to convert some arbitrary collection to a list, with the collector turning it back into a collection type again (not necessarily the same.)

For example, the iterator could map from a tree to a list, and the collector then to an array. Or if you already have a list and want a list back, you could pass in the identity function for those.

One call could be the following:

    sort id id (>) somelist
Hope it isn't too unreadable.

Edit: Adjusted the order of the arguments, as the original order wouldn't work too well with partial application.

Some native JS constructs are class-based (or constructed using the new keyword). Promises, for example [0]. Nothing wrong the odd class here and there.

Though I would be wary of introducing patterns and paradigms that make sense in a different language when Typescript offers an ultimately simpler solution. Working against the grain helps nobody. Goes for both OOP and FP, really.

ES modules, functions, and well designed TS models get you 95% of the way.

0 - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

An ES module is encapsulated enough. You can dependency inject with them as you wish. It's like having a class, no need for a class inside a class.

The biggest argument is that my functional-ish code is always 3x shorter with the same features, though.

This. Creating classes to wrap dependencies is a pattern only needed because of language limitations. With JS/TS, you can mock at the import statement level, so no need to twist your code to abstract away importing.

Also, even if you didn't want to mock that way, you can get dependency injection with functions just by taking a parameter for a dependency. If dependency injection is the only reason you have to use a class, you probably shouldn't use a class.

Request-scoped DI (as seen in ASP.NET MVC) is great on the backend for servicing requests. You can ask for e.g. a class representing the current user information to be injected anywhere, or to keep track of request-associated state like opentelemetry spans, or a transaction, etc. The alternative is to pass the user information class or transaction to all other services, which can be annoying

Its rarely seen in the ecosystem as a solution, unfortunately (everyone is passing all arguments all the time), but its one of the rare places where this is still useful. I've had bad experience with the alternative (continuation local storage) and its not nearly as elegant.

The IoC is a nice paradigm that I tried using with a couple different libraries in JS, but they all felt like they had missing features and sometimes were annoying to run tests with (depending on the how they implemented the IoC container within their frameworks). The work Microsoft puts into their web frameworks to make them so cohesive with the rest of their ecosystem libraries is sometimes underrated
Yeah, the designs of DI libraries in TS land often leave me wondering what the author was thinking.
How do you dependency inject a ES module?
You'll export a function from your esModule if you want to inject

export const myFn = (...deps) =>

You don't need anything else, for encapsulation you have the EsModule, that what OP meant.

ah, thanks, I thought it was something different.
You have dynamic imports too if you need IoC.
I think the word "interface" means something conceptually different in different languages.

in Java it means "you should implement this contract"

in Typescript it means "this data type has this particular shape. It may have methods, too".

in Go it means "I'd like users of this code to implement these methods" (client interfaces).

In all cases you have to work differently with them. It's not even about OOP, I think, to the point where I'm not sure now if the keyword 'interface' is part of OOP at all.

We use Classes extensively in our code because it is an Electron app that interfaces with hardware. OO Typescript is incredibly useful, as we have clearly defined objects and inheritance schemes that would be a nightmare in native JS. The syntactic sugar of TS classes delivers an enormously powerful verification system.
I like classes but I'm in the minority. They are just syntactic sugar over functions, but I like the explicitness of them. If a closure works for you, that's great but there isn't an objective reason to use one over the other.
Interfaces are late bound, which, in conjunction with the concept of object identity and state, is OO. Subclassing is merely one specific approach to code reuse within the OO paradigm.