前書き
日常を過ごしていて、「TypeScriptでDIできたらいいな~。できればvanillaで。」なんて思うことはありませんか?
私はふと道を歩いているときや湯船に浸かっているときになります。
なんて思いながら調べてみるとInversifyJSやtsyringeに辿りつきますが、vanillaじゃないですよね。
なので今回は足りない脳みそを濡れた雑巾のように搾り取り、強引に依存性を注入するコードを書きました。
ですので、この文章は搾りカスの出涸らしが書いています。いつもより稚拙な文章ですがお付き合いください。
追記(2022/05/15): 続きを書きました。 Re:【TypeScript】非情報系卒の駆け出しエンジニアなりにDIするコードをvanillaで書いてみた
要件
- パッケージのインストールを用いない
- 使う側になるべく依存性の注入を意識させない
コード
対象インスタンスを継承元が同じクラスの異なるインスタンスに渡し、対象インスタンスのプロパティを変化させることで依存性の注入が出来ているかを確認します。
なお、以下に示すコードファイルはすべて同階層に置いています。
target.ts
対象インスタンスとなるクラスです。
変化させる対象のプロパティは param
で、インクリメントとデクリメントができるようにしてあります。
export class Target {
private param: number;
constructor() {
this.param = 0;
}
increment() {
this.param++;
}
decrement() {
this.param--;
}
get currentParam() {
return this.param;
}
}
cls.ts
依存性注入するクラスです。
ファイル名が安直でウケますね。
継承先にDIを意識させたくなかったため、デコレータ+implementsを用いる方向で試行錯誤したのですが、力不足で実現できませんでした。
なので継承元にコンストラクタとは別に init()
を作成し、それを呼び出すことで継承先がDIを意識しにくくしつつ依存性注入を行っています。そうなるとメソッド名は _init()
にしたほうがよさそうですね。
また、 init()
が呼び出される前提のため、対象インスタンスを格納する target!: Target;
はその場しのぎをしちゃっています。危ないですね。
init()
は下に記載する呼び出すタイミングでコードを簡略化するために this
を返却しています。
ここではAとBの2つのクラスを定義しています。これらを呼び出します。
import { Target } from './target';
export abstract class Base {
abstract member: string;
target!: Target;
constructor() {}
init(target: Target): this {
this.target = target;
return this;
}
increment(): void {
this.target.increment();
}
decrement(): void {
this.target.decrement();
}
}
export class A extends Base {
member = 'A';
constructor() {
super();
}
}
export class B extends Base {
member = 'B';
constructor() {
super();
}
}
main.ts
実行するファイルです。
変数名が雑で申し訳ないです。短いコードなので許してください。
DIできているかを確認するために main()
でtargetインスタンスを作成しています。
setting()
で注入を行いつつインスタンスを作成して返します。
また、init()
したい関係でBaseから継承されているクラスのみを依存性注入対象としたいため <T extends Base>
しています。
注入対象クラスはインスタンスを受け取る形にしたくないため、型
interface Type<T> extends Function {
new (...args: any[]): T;
}
を定義しています。これは
>>> import { A } from './cls';
>>> typeof A
Function
より、Function型を継承していると捉えています。
"捉えています"というのは、このコードはAngular type.tsからまんまパクっているためです。
import { Base, A, B } from './cls';
import { Target } from './target';
interface Type<T> extends Function {
new (...args: any[]): T;
}
function setting<T extends Base>(target: Target, classes: Type<T>[]): T[] {
return classes.map(t => (new t()).init(target));
}
function main() {
const target = new Target();
const d = setting(target, [A, B]);
d.forEach(c => c.increment());
console.log(target.currentParam); // 2
}
main();
AとBそれぞれで1回ずつ increment()
しているため2が出力されます。
おわりに
問題点だらけなので参考程度に留めてください。