Open7

TypeScriptで簡易的なDIコンテナ

Nakano as a ServiceNakano as a Service
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))

Nakano as a ServiceNakano as a Service

これを素朴に書こうとするとこうなる

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))
Nakano as a ServiceNakano as a Service

こうすれば無駄にクラスをインスタンス化しなくてよくなり率がよい


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" })
Nakano as a ServiceNakano as a Service

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" }))
Nakano as a ServiceNakano as a Service
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);

Nakano as a ServiceNakano as a Service
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];
};

Nakano as a ServiceNakano as a Service
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,
});