Open7
TypeScriptで簡易的なDIコンテナ
class Container<Deps, Defaults = {}> {
private _defaults: Defaults
defaults<NewDefaults>(d: NewDefaults) {
this._defaults = d as unknown as Defaults
return this as unknown as Omit<Container<Deps, NewDefaults>, "defaults">
}
build<Args extends any[], Return>(
f: (deps: Deps & Defaults, ...args: Args) => Return,
): (deps: Deps & Partial<Defaults>) => (...args: Args) => Return {
return (deps: Deps & Partial<Defaults>) =>
(...args: Args) =>
f({ ...this._defaults, ...deps }, ...args)
}
}
function depends<Deps extends {}>() {
return new Container<Deps>()
}
const constructTestFn = depends<{ a: number }>()
.defaults({ b: "foo" })
.build(({ a, b }, a1: number) => {
return `${a} ${b} ${a1}`
})
const testFn = constructTestFn({ a: 1, b: "bar" })
console.log(testFn(1))
これを素朴に書こうとするとこうなる
const constructTestFn2 =
(deps: { a: number; b: string | undefined }) => (a1: number) => {
if (deps.b === undefined) {
deps.b = "foo"
}
return `${deps.a} ${deps.b} ${a1}`
}
const testFn2 = constructTestFn2({ a: 1, b: "bar" })
console.log(testFn2(1))
こうすれば無駄にクラスをインスタンス化しなくてよくなり率がよい
class Container2<Deps> {
build<Defaults, Args extends any[], Return>(
defaults: Defaults,
f: (deps: Deps & Defaults, ...args: Args) => Return,
): (deps: Deps & Partial<Defaults>) => (...args: Args) => Return {
return (deps: Deps & Partial<Defaults>) => {
const allDeps = { ...defaults, ...deps }
return (...args: Args) => f(allDeps, ...args)
}
}
}
const container2 = new Container2()
function depends2<Deps extends {}>() {
return container2 as Container2<Deps>
}
const constructTestFn2 = depends2<{ a: number }>().build(
{ b: "foo" },
({ a, b }, a1: number) => {
return `${a} ${b} ${a1}`
},
)
const testFn2 = constructTestFn2({ a: 1, b: "bar" })
depsに関数を渡せるようにすればデフォルト値を用いてdependenciesを組み立てることも可能
class Builder<Requires extends Record<string, unknown>> {
build<
Defaults extends Record<string, unknown>,
Args extends unknown[],
Return,
>(
defaults: Defaults,
f: (deps: Requires & Defaults, ...args: Args) => Return,
): (
deps:
| (Requires & Partial<Defaults>)
| ((defaults: Defaults) => Requires & Partial<Defaults>),
) => (...args: Args) => Return {
return (
deps:
| (Requires & Partial<Defaults>)
| ((defaults: Defaults) => Requires & Partial<Defaults>),
) => {
const allDeps = {
...defaults,
...(typeof deps === "function" ? deps(defaults) : deps),
}
return (...args: Args) => f(allDeps, ...args)
}
}
}
const builder = new Builder()
function requires<Requires extends Record<string, unknown> = {}>() {
return builder as Builder<Requires>
}
const constructTestFn = requires<{ a: number }>().build(
{ b: "foo" },
({ a, b }, a1: number) => {
return `${a} ${b} ${a1}`
},
)
const testFn1 = constructTestFn({ a: 1, b: "bar" })
const testFn2 = constructTestFn((d) => ({ a: d.b.length, b: "bar" }))
const marker = <T extends Record<string, unknown>>(): T => null as unknown as T;
const constructor = <
Defaults extends Record<string, unknown>,
Dependencies extends Record<string, unknown>,
Args extends unknown[],
Return,
>(
defaults: Defaults,
_marker: () => Dependencies,
fn: (deps: Dependencies & Defaults, ...args: Args) => Return,
) => {
return (
deps: Dependencies & Partial<Defaults>,
) =>
(...args: Args) => fn({ ...defaults, ...deps }, ...args);
};
const constructDoProcess = constructor(
{
getUser,
},
marker<{
getConfig: GetConfig;
}>,
({ getUser, getConfig }, x: number) => {
const user = getUser();
const config = getConfig();
console.log(user, config);
return x;
},
);
const doProcess = constructDoProcess({
getConfig: getConfigImpl,
});
doProcess(2);
const needsSymbol = Symbol("needs");
type Needs<T> = typeof needsSymbol & {
_type: T;
};
const needs = <T>() => needsSymbol as Needs<T>;
function constructorOf<
T extends Record<string, unknown | Needs<unknown>>,
Args extends unknown[],
Return,
>(
depsAndNeeds: T,
f: (deps: DepsOf<T>, ...args: Args) => Return,
) {
return (overwrite: Merge<OverwriteOf<T>>) => {
const newDeps = { ...depsAndNeeds, ...overwrite } as DepsOf<T>;
return f.bind(null, newDeps);
};
}
const constructSampleFunc = constructorOf(
{
dep1: "abc",
dep2: needs<number>(),
},
({ dep1, dep2 }, arg1: number) => {
return dep1 + dep2 + arg1;
},
);
// markerは必須、dep1は任意
const sampleFunc = constructSampleFunc({
dep1: "def",
dep2: 2,
});
// {
// dep1: string;
// dep2: Needs<number>;
// }
// を
// {
// dep1?: string;
// dep2: number;
// }
// に変換する
type OverwriteOf<T extends Record<string, unknown | Needs<unknown>>> =
& {
[K in keyof T as T[K] extends Needs<unknown> ? K : never]: T[K] extends
Needs<infer U> ? U
: T[K];
}
& {
[K in keyof T as T[K] extends Needs<unknown> ? never : K]?: T[K];
};
type X = OverwriteOf<{
dep1: string;
dep2: Needs<number>;
}>;
type Z = Merge<X>;
type DepsOf<T extends Record<string, unknown | Needs<unknown>>> = {
[K in keyof T]: T[K] extends Needs<infer U> ? U : T[K];
};
type Y = DepsOf<{
dep1: string;
dep2: Needs<number>;
}>;
type Merge<T> = {
[K in keyof T]: T[K];
};
function constructorOf<
T extends Record<string, unknown | Needs<unknown>>,
>(
depsAndNeeds: T,
) {
return (overwrite: Merge<OverwriteOf<T>>) => {
const newDeps = { ...depsAndNeeds, ...overwrite } as DepsOf<T>;
return newDeps;
};
}
const constructSample = constructorOf(
{
dep1: "abc",
dep2: needs<number>(),
},
);
// markerは必須、dep1は任意
const sample = constructSample({
dep1: "def",
dep2: 2,
});