typeofClass<T>Class<T>は抽象クラスに一致しませんこの章では、値としてのクラスについて探ります。
次のクラスを考えてみましょう。
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}この関数はクラスを受け取り、そのインスタンスを作成します。
function createPoint(PointClass: ???, x: number, y: number) {
return new PointClass(x, y);
}Pointまたはそのサブクラスにする場合、パラメータPointClassにはどのような型を使用すべきでしょうか?
typeof§7.7「2つの言語レベル:動的 vs. 静的」では、TypeScriptの2つの言語レベルについて調べました。
クラスPointは2つのものを生成します。
PointPointのインスタンスのためのインターフェースPointPointの記述場所によって、意味が異なります。そのため、PointClassに型Pointを使用することはできません。これはクラスPoint自体ではなく、クラスPointのインスタンスに一致します。
代わりに、型演算子typeof(JavaScriptにも存在するTypeScript構文のもう一つの要素)を使用する必要があります。typeof vは、動的(!)値vの型を表します。
function createPoint(PointClass: typeof Point, x: number, y: number) { // (A)
return new PointClass(x, y);
}
// %inferred-type: Point
const point = createPoint(Point, 3, 6);
assert.ok(point instanceof Point);コンストラクタ型リテラルは、接頭辞newが付いた関数型リテラルです(A行)。この接頭辞は、PointClassがnewを介して呼び出されなければならない関数であることを示しています。
function createPoint(
PointClass: new (x: number, y: number) => Point, // (A)
x: number, y: number
) {
return new PointClass(x, y);
}インターフェースとオブジェクトリテラル型(OLT)のメンバーには、メソッドシグネチャとコールシグネチャが含まれることを思い出してください。コールシグネチャにより、インターフェースとOLTは関数を記述できます。
同様に、コンストラクタシグネチャにより、インターフェースとOLTはコンストラクタ関数を記述できます。これは、接頭辞newが追加されたコールシグネチャのように見えます。次の例では、PointClassはコンストラクタシグネチャを持つオブジェクトリテラル型を持っています。
function createPoint(
PointClass: {new (x: number, y: number): Point},
x: number, y: number
) {
return new PointClass(x, y);
}Class<T>得られた知識を用いて、型パラメータTを導入することにより、値としてのクラスのためのジェネリック型を作成できます。
type Class<T> = new (...args: any[]) => T;型エイリアスの代わりに、インターフェースを使用することもできます。
interface Class<T> {
new(...args: any[]): T;
}Class<T>は、そのインスタンスが型Tと一致するクラスのための型です。
Class<T>を使用すると、createPoint()のジェネリックバージョンを書くことができます。
function createInstance<T>(AnyClass: Class<T>, ...args: any[]): T {
return new AnyClass(...args);
}createInstance()は次のように使用されます。
class Person {
constructor(public name: string) {}
}
// %inferred-type: Person
const jane = createInstance(Person, 'Jane');createInstance()は、関数によって実装されたnew演算子です。
Class<T>を使用してキャストを実装できます。
function cast<T>(AnyClass: Class<T>, obj: any): T {
if (! (obj instanceof AnyClass)) {
throw new Error(`Not an instance of ${AnyClass.name}: ${obj}`)
}
return obj;
}cast()を使用すると、値の型をより具体的なものに変更できます。これは、静的に型を変更し、動的なチェックを実行するため、ランタイムでも安全です。次のコードは例を示しています。
function parseObject(jsonObjectStr: string): Object {
// %inferred-type: any
const parsed = JSON.parse(jsonObjectStr);
return cast(Object, parsed);
}Class<T>とcast()の1つのユースケースは、型安全なマップです。
class TypeSafeMap {
#data = new Map<any, any>();
get<T>(key: Class<T>) {
const value = this.#data.get(key);
return cast(key, value);
}
set<T>(key: Class<T>, value: T): this {
cast(key, value); // runtime check
this.#data.set(key, value);
return this;
}
has(key: any) {
return this.#data.has(key);
}
}TypeSafeMapの各エントリのキーはクラスです。そのクラスはエントリの値の静的型を決定し、ランタイムでのチェックにも使用されます。
これは動作中のTypeSafeMapです。
const map = new TypeSafeMap();
map.set(RegExp, /abc/);
// %inferred-type: RegExp
const re = map.get(RegExp);
// Static and dynamic error!
assert.throws(
// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'Date'.
() => map.set(Date, 'abc'));Class<T>は抽象クラスに一致しませんClass<T>が期待される場合、抽象クラスを使用することはできません。
abstract class Shape {
}
class Circle extends Shape {
// ···
}
// @ts-expect-error: Type 'typeof Shape' is not assignable to type
// 'Class<Shape>'.
// Cannot assign an abstract constructor type to a non-abstract
// constructor type. (2322)
const shapeClasses1: Array<Class<Shape>> = [Circle, Shape];なぜでしょうか?その理由は、コンストラクタ型リテラルとコンストラクタシグネチャは、実際にnewで呼び出すことができる値に対してのみ使用するべきであるためです(詳細情報を含むGitHub issue)。
これは回避策です。
type Class2<T> = Function & {prototype: T};
const shapeClasses2: Array<Class2<Shape>> = [Circle, Shape];このアプローチの欠点
instanceofチェック(右辺オペランドとして)に使用できません。