Genericsを使ったクラスのコンストラクタの引数の制約

以下の条件を満たすItem<V>型を受け入れるGeneric Typeを持つクラスを定義するつもりで、以下のクラス Generator を定義しました。

  • Generator クラスは Item<unknown> 型を受け入れるGeneric Typeを持つ(Generic constraints
  • Item<V> の Generic Type(V)の型は制限しないこと(= unknown type)としたい
  • コンストラクタで受け取るT型 (= extends Item<unknown>) と Processor<T> 型において、同一のTであるという制約を設けたい
interface Item<V> {
  value: V
  clone: () => Item<V>
}

class Processor<U extends Item<unknown>> {
  setObject(obj: U) { }
}

class Generator<T extends Item<unknown>> {
  constructor(private obj: T, private processor: Processor<T>) {}

  process() {
    const cloned = this.obj.clone()
    this.processor.setObject(cloned)
  }
}

しかし、上記のような定義では、process()this.processor.setObject(cloned)においてType Errorとなります。

  • cloned の型は Item<unknown> と評価されている状態(this.objItem<unknown> として評価されるため、Item<unknown> = {value: unknown, clone: () => Item<unknown> と評価される)
    • (わたしはここ cloned の型は T と評価されるべきと勘違いしてしまっていたのだった)
  • 一方で this.processor.setObject の第一引数は T 型を要求している

T型よりもItem<unknown>のほうが取りうる型の範囲が広いので、以下のType Errorが表示されます。

Argument of type 'Item<unknown>' is not assignable to parameter of type 'T'.
  'Item<unknown>' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'Item<unknown>'.

よく考えてみれば、制約をしたいのは Item<V> そのものではなく、V なので、以下のように修正してみます。

class Generator<T> {
  constructor(private obj: Item<T>, private processor: Processor<Item<T>>) {}

  process() {
    const cloned = this.obj.clone()
    this.processor.setObject(cloned)
  }
}

以下のようなItemを実装したクラスを定義したとき

class NumberItem implements Item<number> {
  constructor(private _value: number) {}

  get value() { return this._value }
  clone() { return new NumberItem(this._value) }
}

class StringItem implements Item<string> {
  constructor(private _value: string) {}

  get value() { return this._value }
  clone() { return new StringItem(this._value) }
}

以下のように記述すると、期待通りの評価結果となりました。コンストラクタに渡した引数に対して型推論が行われるので、 Generator<T>T 型を明示的に与えなくても型による制約を得られます。

// T型(= NumberItem) が一致するので型チェックはパスする
const testB1 = new GeneratorB(new NumberItem(1), new Processor<NumberItem>)

// Type Error (T型が一致しない)
const testB2 = new GeneratorB(new NumberItem(1), new Processor<StringItem>)

気が付けば「たしかにそれはそう」となりそうなことですが、なかなか気づけずにはまってしまったのでここにメモを残しておきます。

ではでは。

このカウンタは @piyoppi/counter-tools を使っています。

クリックすると匿名でいいねできます。