Skip to content

Commit 18108b3

Browse files
committed
docs: Inject refactor
1 parent 96f5ce1 commit 18108b3

File tree

4 files changed

+91
-6
lines changed

4 files changed

+91
-6
lines changed

src/content/003-getting-started-with-inject.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@ excerpt: Dependency injection and Inversion of control is a common practice that
1111

1212

1313
## Injectable services
14-
An _injectable service_ is basically a class, decorated with the `@Injectable()` decorator. If you decorate a class, its injectable options (e.g. lifetime) and constructor argument types will be stored and the injector will be able to instantiate a new instance any time. Constructor arguments should be also _injectable services_ and they will be resolved recursively. Take a look at the following example and you'll get the idea:
14+
An _injectable service_ is basically a class, decorated with the `@Injectable()` decorator. If you decorate a class, its injectable options (e.g. lifetime) will be stored and the injector will be able to instantiate a new instance any time. You can also decorate properties with the `@Injected(Type)` decorator to inject them after instantiating the object. Take a look at the following example and you'll get the idea:
1515

1616
```ts
1717
const injector = new Injector()
1818
@Injectable()
1919
class Service1 {
20-
constructor(public service2: Service2, public service3: Service3) {}
20+
@Injected(Service2)
21+
public service2!: Service2
22+
23+
@Injected(Service3)
24+
public service2!: Service3
2125
}
2226
@Injectable()
2327
class Service2 {
@@ -54,7 +58,10 @@ The package defines four types of lifecycle:
5458
- **Singleton** injectables are hoisted to the root injector. If you request a singleton, the injector will check create the instance in it's highest parent - and also returns it from there, if already exists.
5559
- **Explicit** values are not really injectables - you can call `injector.setExplicitInstance(myServiceInstance)` to set up an instance manually. Just like scoped services, explicit instances will be returned from the current scope only.
5660

57-
## Extension methods
61+
## ~~Extension methods~~
62+
63+
[(We have already said goodbye to extension methods)](/008-byebye-extension-methods/)
64+
5865
A simple injector can be easily extended by 3rd party packages with extension methods, just like the FuryStack packages. These extension methods usually provides a _shortcut_ of an instance or sets up a preconfigured explicit instance of a service. You can build clean and nice fluent API-s in that way - you can get the idea from one of the [FuryStack Injector Extensions](https://github.com/furystack/furystack/blob/develop/packages/rest-service/src/injector-extensions.ts)
5966

6067
You find more inject-related articles [here](/tags/inject) or check out the package at NPM

src/content/008-byebye-extension-methods.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ draft: false
99
excerpt: Using extension methods was fun at the beginning but I've ran into more and more problems with them
1010
---
1111

12-
### The Heritage from C#
12+
## The Heritage from C#
1313

1414
It's not a big sectet that one of the main inspiration for FuryStack is the .NET (legacy and Core) stack where extension methods are quite common. The same can achieved in the JS world where everything is possible, every prototype can be hacked and it can be also type-safe.
1515

16-
### The Problem
16+
## The Problem
1717

1818
The main problem in short that extending another module is _not officially supported_ by Typescript - however the type system can be hacked like _it was_ in FuryStack and as you can see in the following example:
1919

@@ -42,7 +42,7 @@ const result = await injector.isAuthorized('admin');
4242

4343
...well, it works but I've ran into unexpected issues with conflicting import declarations and overrides. The problems appeared random, but somewhat based on the running context (ts-node, jest, browser) and it started to block dependency upgrades...
4444

45-
### How it works now
45+
## How it works now
4646

4747
The extensions has been replaced by _helpers_, as you can see in the following example. These helpers are simple shortcuts - you should usually pass the injector (or the related manager class) to them as a parameter.
4848
The imports are clear, as well as the execution path and the types.

src/content/009-inject-refactor.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
layout: post
3+
title: A little bit of Inject refactor
4+
author: [gallayl]
5+
tags: ['inject']
6+
image: img/009-inject-refactor.jpg
7+
date: '2022-08-12T18:00:00.257Z'
8+
draft: false
9+
excerpt: Emitting decorator type data is doomed :(
10+
---
11+
12+
## Why
13+
14+
As you can see, in FuryStack I've tried to take some steps to use only standardized APIs to maintain the supportability. As I started to work with new tools and framework, I've found some bottlenecks. The first big bad was the hacky [extension method support](/008-byebye-extension-methods/) that I've introduced in the beginning of the project, but I've found an another black sheep in the heart of the Typescript ecosystem - Decorator support.
15+
16+
In short: "[The emitDecoratorMetadata flag is intentionally not supported.](https://github.com/evanw/esbuild/issues/257#issuecomment-658053616)"
17+
18+
## Emm ok, what now? 😕
19+
20+
We had a feature in `Inject` that *was* build on a top of emitting type data and that was *constructor injection*. The syntax was like:
21+
22+
```ts
23+
const injector = new Injector()
24+
@Injectable()
25+
class Service1 {
26+
constructor(public service2: Service2, public service3: Service3) {}
27+
}
28+
@Injectable()
29+
class Service2 {
30+
public value = 'foo'
31+
}
32+
@Injectable()
33+
class Service3 {
34+
public value = 'bar'
35+
}
36+
37+
expect(injector.getInstance(Service1).service2.value).toBe('foo')
38+
expect(injector.getInstance(Service1).service2.value).toBe('bar')
39+
```
40+
41+
That's clear that we can't use this if we loose type data at runtime, so the idea was to pass down the constructor object at runtime - and try to maintain the simplicity of the old API.
42+
43+
## The new `Injected()` properties ✨
44+
45+
...so a new property-level decorator called `Injected()` was born.
46+
The very same behavior with the new API looks like this:
47+
48+
```ts
49+
const injector = new Injector()
50+
@Injectable()
51+
class Service1 {
52+
@Injected(Service2)
53+
public service2!: Service2
54+
55+
@Injected(Service3)
56+
public service2!: Service3
57+
}
58+
@Injectable()
59+
class Service2 {
60+
public value = 'foo'
61+
}
62+
@Injectable()
63+
class Service3 {
64+
public value = 'bar'
65+
}
66+
67+
expect(injector.getInstance(Service1).service2.value).toBe('foo')
68+
expect(injector.getInstance(Service1).service2.value).toBe('bar')
69+
```
70+
71+
### Some pros 👌
72+
- The consrtuctor instance is passed down as a variable - without emitting non-standard metadata and other black magic
73+
- The main behavior (lifetime, recursive resolution, etc...) remains the same
74+
75+
### ...and a few drawbacks 😿
76+
- Properties will be injected **after** constructing the instance - it means that you cannot use them in the constructor
77+
- A breaking change - again...
78+
- See the `!` operator? Yeah, it's a kind of "shortcut" for now...
1.58 MB
Loading

0 commit comments

Comments
 (0)