クラス

背景資料
クラス (MDN)

TypeScriptは、ES2015で導入されたclassキーワードを完全にサポートしています。

他のJavaScript言語機能と同様に、TypeScriptは型注釈やその他の構文を追加して、クラスと他の型との関係を表現できるようにします。

クラスメンバー

これが最も基本的なクラス、つまり空のクラスです。

ts
class Point {}
Try

このクラスはまだあまり役に立たないので、メンバーを追加することから始めましょう。

フィールド

フィールド宣言は、クラスにpublicな書き込み可能なプロパティを作成します。

ts
class Point {
x: number;
y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;
Try

他の場所と同様に、型注釈はオプションですが、指定しない場合は暗黙的にanyになります。

フィールドには初期化子を持たせることもできます。これらはクラスがインスタンス化されるときに自動的に実行されます。

ts
class Point {
x = 0;
y = 0;
}
 
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);
Try

constlet、およびvarと同様に、クラスプロパティの初期化子は、その型を推論するために使用されます。

ts
const pt = new Point();
pt.x = "0";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

--strictPropertyInitialization

strictPropertyInitialization設定は、クラスフィールドをコンストラクターで初期化する必要があるかどうかを制御します。

ts
class BadGreeter {
name: string;
Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.
}
Try
ts
class GoodGreeter {
name: string;
 
constructor() {
this.name = "hello";
}
}
Try

フィールドはコンストラクター自体で初期化する必要があることに注意してください。 TypeScriptは、コンストラクターから呼び出すメソッドを分析して初期化を検出することはありません。これは、派生クラスがこれらのメソッドをオーバーライドし、メンバーを初期化できない可能性があるためです。

コンストラクター以外の手段(たとえば、外部ライブラリがクラスの一部を埋めている場合など)でフィールドを確実に初期化する場合は、definite assignment assertion operatorである!を使用できます。

ts
class OKGreeter {
// Not initialized, but no error
name!: string;
}
Try

readonly

フィールドには、readonly修飾子を付けることができます。これにより、コンストラクター外でのフィールドへの代入を防ぎます。

ts
class Greeter {
readonly name: string = "world";
 
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
 
err() {
this.name = "not ok";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.
Try

コンストラクター

背景資料
コンストラクター(MDN)

クラスコンストラクターは、関数と非常によく似ています。 型注釈、デフォルト値、オーバーロードを使用してパラメーターを追加できます。

ts
class Point {
x: number;
y: number;
 
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
Try
ts
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
Try

クラスコンストラクターのシグネチャと関数のシグネチャには、いくつかの違いがあります。

  • コンストラクターは型パラメーターを持つことはできません。これらは外側のクラス宣言に属します。これについては後で学習します。
  • コンストラクターは戻り値の型注釈を持つことはできません。クラスインスタンス型は常に返されるものです。

スーパーコール

JavaScriptと同様に、基底クラスがある場合、コンストラクタの本体でthis.メンバを使用する前にsuper();を呼び出す必要があります。

ts
class Base {
k = 4;
}
 
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
Try

superの呼び出し忘れはJavaScriptで起こりがちなミスですが、TypeScriptは必要な場合にそれを教えてくれます。

メソッド

背景資料
メソッド定義

クラスの関数プロパティはメソッドと呼ばれます。メソッドは、関数やコンストラクタと同じ型注釈をすべて使用できます。

ts
class Point {
x = 10;
y = 10;
 
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
Try

標準の型注釈を除けば、TypeScriptはメソッドに新しいものを何も追加しません。

メソッド本体内では、フィールドや他のメソッドにthis.経由でアクセスする必要があることに注意してください。メソッド本体内の修飾されていない名前は、常に外側のスコープの何かを参照します。

ts
let x: number = 0;
 
class C {
x: string = "hello";
 
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
}
}
Try

ゲッター/セッター

クラスはアクセサーを持つこともできます。

ts
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
Try

追加のロジックがない、フィールドにバックアップされたget/setのペアは、JavaScriptでは非常にまれにしか役に立ちません。get/set操作中に追加のロジックを追加する必要がない場合は、publicフィールドを公開しても構いません。

TypeScriptには、アクセサーに関する特別な推論ルールがいくつかあります。

  • getが存在するがsetが存在しない場合、プロパティは自動的にreadonlyになります。
  • セッターパラメータの型が指定されていない場合、ゲッターの戻り型から推論されます。
  • ゲッターとセッターは、同じメンバーの可視性を持つ必要があります。

TypeScript 4.3以降、取得と設定で異なる型を持つアクセサーを持つことができます。

ts
class Thing {
_size = 0;
 
get size(): number {
return this._size;
}
 
set size(value: string | number | boolean) {
let num = Number(value);
 
// Don't allow NaN, Infinity, etc
 
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
 
this._size = num;
}
}
Try

インデックスシグネチャ

クラスはインデックスシグネチャを宣言できます。これらは他のオブジェクト型のインデックスシグネチャと同じように機能します。

ts
class MyClass {
[s: string]: boolean | ((s: string) => boolean);
 
check(s: string) {
return this[s] as boolean;
}
}
Try

インデックスシグネチャ型はメソッドの型もキャプチャする必要があるため、これらの型を効果的に使用するのは簡単ではありません。一般に、インデックス付きデータをクラスインスタンス自体ではなく、別の場所に保存する方が優れています。

クラスの継承

オブジェクト指向の機能を持つ他の言語と同様に、JavaScriptのクラスは基底クラスから継承できます。

implements

implements句を使用して、クラスが特定のinterfaceを満たしているかどうかを確認できます。クラスが正しく実装できなかった場合、エラーが発行されます。

ts
interface Pingable {
ping(): void;
}
 
class Sonar implements Pingable {
ping() {
console.log("ping!");
}
}
 
class Ball implements Pingable {
Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.
pong() {
console.log("pong!");
}
}
Try

クラスは複数のインターフェースを実装することもできます。例:class C implements A, B {

注意事項

implements句は、クラスがインターフェース型として扱えるかどうかを確認するだけであるということを理解することが重要です。クラスまたはそのメソッドの型をまったく変更しません。よくあるエラーの原因は、implements句がクラスの型を変更すると想定することです。そうではありません!

ts
interface Checkable {
check(name: string): boolean;
}
 
class NameChecker implements Checkable {
check(s) {
Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.
// Notice no error here
return s.toLowerCase() === "ok";
any
}
}
Try

この例では、おそらくsの型がcheckname: stringパラメータの影響を受けることを期待していました。そうではありません。implements句は、クラス本体がどのようにチェックされるか、またはその型がどのように推論されるかを変更しません。

同様に、オプションのプロパティを持つインターフェースを実装しても、そのプロパティは作成されません。

ts
interface A {
x: number;
y?: number;
}
class C implements A {
x = 0;
}
const c = new C();
c.y = 10;
Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.
Try

extends

背景資料
extendsキーワード(MDN)

クラスは基底クラスからextendできます。派生クラスは、基底クラスのすべてのプロパティとメソッドを持ち、追加のメンバーを定義することもできます。

ts
class Animal {
move() {
console.log("Moving along!");
}
}
 
class Dog extends Animal {
woof(times: number) {
for (let i = 0; i < times; i++) {
console.log("woof!");
}
}
}
 
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);
Try

メソッドのオーバーライド

背景資料
superキーワード(MDN)

派生クラスは、基底クラスのフィールドまたはプロパティをオーバーライドすることもできます。基底クラスのメソッドにアクセスするには、super.構文を使用できます。JavaScriptのクラスは単純なルックアップオブジェクトであるため、「スーパーフィールド」の概念はないことに注意してください。

TypeScriptは、派生クラスが常に基底クラスのサブタイプであることを強制します。

たとえば、メソッドをオーバーライドする合法的な方法を次に示します。

ts
class Base {
greet() {
console.log("Hello, world!");
}
}
 
class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}
 
const d = new Derived();
d.greet();
d.greet("reader");
Try

派生クラスが基底クラスの契約に従うことが重要です。派生クラスのインスタンスを基底クラスの参照を通じて参照するのは非常に一般的であり、常に合法であることを忘れないでください。

ts
// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();
Try

DerivedBaseの契約に従わなかった場合はどうなるでしょうか?

ts
class Base {
greet() {
console.log("Hello, world!");
}
}
 
class Derived extends Base {
// Make this parameter required
greet(name: string) {
Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.
console.log(`Hello, ${name.toUpperCase()}`);
}
}
Try

エラーにもかかわらずこのコードをコンパイルした場合、このサンプルはクラッシュします。

ts
const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();
Try

型のみのフィールド宣言

target >= ES2022またはuseDefineForClassFieldstrueの場合、クラスフィールドは親クラスのコンストラクタが完了した後に初期化され、親クラスによって設定された値を上書きします。これは、継承されたフィールドに対してより正確な型を再宣言したいだけの場合に問題となる可能性があります。これらのケースを処理するために、TypeScriptにこのフィールド宣言にランタイム効果がないことを示すためにdeclareを記述できます。

ts
interface Animal {
dateOfBirth: any;
}
 
interface Dog extends Animal {
breed: any;
}
 
class AnimalHouse {
resident: Animal;
constructor(animal: Animal) {
this.resident = animal;
}
}
 
class DogHouse extends AnimalHouse {
// Does not emit JavaScript code,
// only ensures the types are correct
declare resident: Dog;
constructor(dog: Dog) {
super(dog);
}
}
Try

初期化順序

JavaScriptクラスが初期化される順序は、場合によっては驚くべきものです。このコードを考えてみましょう。

ts
class Base {
name = "base";
constructor() {
console.log("My name is " + this.name);
}
}
 
class Derived extends Base {
name = "derived";
}
 
// Prints "base", not "derived"
const d = new Derived();
Try

ここで何が起こったのでしょうか?

JavaScriptで定義されているクラスの初期化順序は次のとおりです。

  • 基底クラスのフィールドが初期化されます。
  • 基底クラスのコンストラクタが実行されます。
  • 派生クラスのフィールドが初期化されます。
  • 派生クラスのコンストラクタが実行されます。

つまり、派生クラスのフィールド初期化がまだ実行されていないため、基底クラスのコンストラクタは自身のコンストラクタ中にnameの自身の値を確認しました。

組み込み型の継承

注:ArrayErrorMapなどの組み込み型から継承する予定がない場合、またはコンパイルターゲットが明示的にES6/ES2015以上に設定されている場合は、このセクションをスキップできます。

ES2015では、オブジェクトを返すコンストラクタは、super(...)のすべての呼び出し元に対してthisの値を暗黙的に置き換えます。生成されたコンストラクタコードがsuper(...)の潜在的な戻り値をキャプチャし、それをthisに置き換える必要があります。

結果として、ErrorArrayなどをサブクラス化すると、期待どおりに機能しなくなる場合があります。これは、ErrorArrayなどのコンストラクタ関数がECMAScript 6のnew.targetを使用してプロトタイプチェーンを調整するためです。ただし、ECMAScript 5でコンストラクタを呼び出すときにnew.targetの値を確認する方法はありません。他のダウンレベルコンパイラも、デフォルトでは通常同じ制限があります。

次のようなサブクラスの場合

ts
class MsgError extends Error {
constructor(m: string) {
super(m);
}
sayHello() {
return "hello " + this.message;
}
}
Try

次のようなことがわかります。

  • これらのサブクラスを構築して返されたオブジェクトでメソッドがundefinedになる場合があるため、sayHelloを呼び出すとエラーになります。
  • instanceofはサブクラスのインスタンスとそのインスタンスの間で壊れるため、(new MsgError()) instanceof MsgErrorfalseを返します。

推奨事項として、super(...)呼び出しの直後にプロトタイプを手動で調整できます。

ts
class MsgError extends Error {
constructor(m: string) {
super(m);
 
// Set the prototype explicitly.
Object.setPrototypeOf(this, MsgError.prototype);
}
 
sayHello() {
return "hello " + this.message;
}
}
Try

ただし、MsgError のサブクラスは、プロトタイプも手動で設定する必要があります。 Object.setPrototypeOf をサポートしないランタイムでは、代わりに __proto__ を使用できる場合があります。

残念ながら、これらの回避策は Internet Explorer 10 以前では機能しません。 プロトタイプからインスタンス自体にメソッドを手動でコピーする(つまり、MsgError.prototypethis にコピーする)ことはできますが、プロトタイプチェーン自体を修正することはできません。

メンバーの可視性

TypeScript を使用して、特定のメソッドまたはプロパティをクラス外のコードから可視にするかどうかを制御できます。

public

クラスメンバーのデフォルトの可視性は public です。 public メンバーはどこからでもアクセスできます。

ts
class Greeter {
public greet() {
console.log("hi!");
}
}
const g = new Greeter();
g.greet();
Try

public はすでにデフォルトの可視性修飾子であるため、クラスメンバーに記述する必要はありませんが、スタイルや読みやすさのために記述することもできます。

protected

protected メンバーは、宣言されているクラスのサブクラスからのみ可視です。

ts
class Greeter {
public greet() {
console.log("Hello, " + this.getName());
}
protected getName() {
return "hi";
}
}
 
class SpecialGreeter extends Greeter {
public howdy() {
// OK to access protected member here
console.log("Howdy, " + this.getName());
}
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();
Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
Try

protected メンバーの公開

派生クラスは、基底クラスのコントラクトに従う必要がありますが、より多くの機能を持つ基底クラスのサブタイプを公開することを選択できます。 これには、protected メンバーを public にすることが含まれます。

ts
class Base {
protected m = 10;
}
class Derived extends Base {
// No modifier, so default is 'public'
m = 15;
}
const d = new Derived();
console.log(d.m); // OK
Try

Derived はすでに m を自由に読み書きできることに注意してください。したがって、これはこの状況の「セキュリティ」を意味のある形で変更するものではありません。 ここで注目すべき主な点は、派生クラスでは、この公開が意図的でない場合は、protected 修飾子を繰り返すように注意する必要があるということです。

階層間の protected アクセス

さまざまな OOP 言語では、基底クラスの参照を介して protected メンバーにアクセスすることが合法であるかどうかについて意見が異なります。

ts
class Base {
protected x: number = 1;
}
class Derived1 extends Base {
protected x: number = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10;
}
f2(other: Derived1) {
other.x = 10;
Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.
}
}
Try

たとえば、Java では、これは合法であると見なされます。 一方、C# と C++ では、このコードは違法であると判断しました。

TypeScript はここで C# および C++ を支持します。なぜなら、Derived2 での x へのアクセスは、Derived2 のサブクラスからのみ合法である必要があり、Derived1 はそのいずれでもないからです。 さらに、Derived1 の参照を介した x へのアクセスが違法である場合(確かにそうである必要があります!)、基底クラスの参照を介したアクセスによって状況が改善されることはありません。

C# の推論の詳細については、派生クラスから protected メンバーにアクセスできないのはなぜですか? も参照してください。

private

privateprotected に似ていますが、サブクラスからのメンバーへのアクセスも許可しません。

ts
class Base {
private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
Property 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.
Try
ts
class Derived extends Base {
showX() {
// Can't access in subclasses
console.log(this.x);
Property 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.
}
}
Try

private メンバーは派生クラスから可視ではないため、派生クラスはそれらの可視性を高めることができません。

ts
class Base {
private x = 0;
}
class Derived extends Base {
Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.
x = 1;
}
Try

インスタンス間の private アクセス

さまざまな OOP 言語では、同じクラスの異なるインスタンスが互いの private メンバーにアクセスできるかどうかについて意見が異なります。 Java、C#、C++、Swift、PHP などの言語ではこれが許可されていますが、Ruby では許可されていません。

TypeScript では、インスタンス間の private アクセスが許可されています。

ts
class A {
private x = 10;
 
public sameAs(other: A) {
// No error
return other.x === this.x;
}
}
Try

注意点

TypeScript の型システムの他の側面と同様に、privateprotected型チェック中にのみ適用されます

これは、in や単純なプロパティルックアップなどの JavaScript ランタイムの構造で、private または protected メンバーにアクセスできることを意味します。

ts
class MySafe {
private secretKey = 12345;
}
Try
js
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private は、型チェック中にブラケット表記を使用してアクセスすることもできます。 これにより、private で宣言されたフィールドは、単体テストなどの目的でアクセスしやすくなる可能性があります。欠点として、これらのフィールドは *ソフトプライベート* であり、プライバシーを厳密に強制するものではありません。

ts
class MySafe {
private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
Property 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.
 
// OK
console.log(s["secretKey"]);
Try

TypeScript の private とは異なり、JavaScript の プライベートフィールド#)はコンパイル後もプライベートのままであり、ブラケット表記アクセスのような前述のエスケープハッチは提供されないため、*ハードプライベート* になります。

ts
class Dog {
#barkAmount = 0;
personality = "happy";
 
constructor() {}
}
Try
ts
"use strict";
class Dog {
#barkAmount = 0;
personality = "happy";
constructor() { }
}
 
Try

ES2021 以前にコンパイルする場合、TypeScript は # の代わりに WeakMaps を使用します。

ts
"use strict";
var _Dog_barkAmount;
class Dog {
constructor() {
_Dog_barkAmount.set(this, 0);
this.personality = "happy";
}
}
_Dog_barkAmount = new WeakMap();
 
Try

クラス内の値を悪意のあるアクターから保護する必要がある場合は、クロージャ、WeakMaps、プライベートフィールドなど、ハードランタイムプライバシーを提供するメカニズムを使用する必要があります。 ランタイム中のこれらの追加のプライバシーチェックは、パフォーマンスに影響を与える可能性があることに注意してください。

静的メンバー

背景資料
静的メンバー (MDN)

クラスには static メンバーを含めることができます。 これらのメンバーは、クラスの特定のインスタンスに関連付けられていません。 クラスコンストラクターオブジェクト自体を介してアクセスできます。

ts
class MyClass {
static x = 0;
static printX() {
console.log(MyClass.x);
}
}
console.log(MyClass.x);
MyClass.printX();
Try

静的メンバーは、同じ publicprotected、および private の可視性修飾子も使用できます。

ts
class MyClass {
private static x = 0;
}
console.log(MyClass.x);
Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.
Try

静的メンバーも継承されます。

ts
class Base {
static getGreeting() {
return "Hello world";
}
}
class Derived extends Base {
myGreeting = Derived.getGreeting();
}
Try

特別な静的名

一般に、Function プロトタイプからプロパティを上書きすることは安全ではなく、不可能です。 クラスは、new で呼び出すことができる関数自体であるため、特定の static 名は使用できません。 namelengthcall などの関数プロパティは、static メンバーとして定義するには無効です。

ts
class S {
static name = "S!";
Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.
}
Try

静的クラスがない理由?

TypeScript(および JavaScript)には、例えば C# のようにstatic classと呼ばれる構造体はありません。

これらの構造体は、これらの言語がすべてのデータと関数をクラス内に強制的に置くようにしているためだけに存在します。TypeScript ではそのような制限がないため、それらは必要ありません。単一のインスタンスしか持たないクラスは、通常、JavaScript/TypeScript では通常のオブジェクトとして表現されます。

例えば、TypeScript では、通常のオブジェクト(またはトップレベル関数)が同じように機能するため、「static class」構文は必要ありません。

ts
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
dosomething() {},
};
Try

クラス内のstaticブロック

static ブロックを使用すると、包含クラス内のプライベートフィールドにアクセスできる独自のスコープを持つステートメントのシーケンスを記述できます。これは、ステートメントを記述するすべての機能を備えた初期化コードを記述でき、変数のリークがなく、クラスの内部に完全にアクセスできることを意味します。

ts
class Foo {
static #count = 0;
 
get count() {
return Foo.#count;
}
 
static {
try {
const lastInstances = loadLastInstances();
Foo.#count += lastInstances.length;
}
catch {}
}
}
Try

ジェネリッククラス

クラスは、インターフェースと同様に、ジェネリックにすることができます。ジェネリッククラスがnewでインスタンス化されると、その型パラメータは関数呼び出しと同じように推論されます。

ts
class Box<Type> {
contents: Type;
constructor(value: Type) {
this.contents = value;
}
}
 
const b = new Box("hello!");
const b: Box<string>
Try

クラスは、インターフェースと同じようにジェネリック制約とデフォルトを使用できます。

静的メンバーの型パラメータ

このコードは合法ではなく、その理由が明確でない可能性があります。

ts
class Box<Type> {
static defaultValue: Type;
Static members cannot reference class type parameters.2302Static members cannot reference class type parameters.
}
Try

型は常に完全に消去されることを忘れないでください。ランタイムでは、Box.defaultValueプロパティのスロットは1つしかありません。これは、Box<string>.defaultValueを設定すると(それが可能であれば)、Box<number>.defaultValueも変更されることを意味します。これは良くありません。ジェネリッククラスのstaticメンバーは、クラスの型パラメータを参照することはできません。

クラス内の実行時におけるthis

背景資料
this キーワード(MDN)

TypeScript は JavaScript のランタイム動作を変更しないこと、そして JavaScript がいくつかの独特なランタイム動作を持つことで有名であることを覚えておくことが重要です。

JavaScript の this の扱いは、確かに通常とは異なります。

ts
class MyClass {
name = "MyClass";
getName() {
return this.name;
}
}
const c = new MyClass();
const obj = {
name: "obj",
getName: c.getName,
};
 
// Prints "obj", not "MyClass"
console.log(obj.getName());
Try

手短に言うと、デフォルトでは、関数内の this の値は、関数がどのように呼び出されたかに依存します。この例では、関数が obj 参照を介して呼び出されたため、this の値はクラスインスタンスではなく obj でした。

これはほとんどの場合、望ましい動作ではありません。TypeScript には、この種のエラーを軽減または防止するためのいくつかの方法が用意されています。

アロー関数

背景資料
アロー関数 (MDN)

this コンテキストを失う方法で頻繁に呼び出される関数がある場合は、メソッド定義の代わりにアロー関数プロパティを使用するのが理にかなっている場合があります。

ts
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());
Try

これにはいくつかのトレードオフがあります。

  • this の値は、TypeScript でチェックされていないコードでも、ランタイムで正しいことが保証されます。
  • 各クラスインスタンスに、この方法で定義された各関数の独自のコピーがあるため、より多くのメモリを使用します。
  • 派生クラスでは super.getName を使用できません。これは、基底クラスのメソッドを取得するためのプロトタイプチェーンにエントリがないためです。

this パラメータ

メソッドまたは関数定義では、this という名前の最初のパラメータには、TypeScript で特別な意味があります。これらのパラメータはコンパイル中に消去されます。

ts
// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
/* ... */
}
Try
js
// JavaScript output
function fn(x) {
/* ... */
}

TypeScript は、this パラメータを持つ関数の呼び出しが正しいコンテキストで行われていることをチェックします。アロー関数を使用する代わりに、メソッド定義に this パラメータを追加して、メソッドが正しく呼び出されることを静的に強制できます。

ts
class MyClass {
name = "MyClass";
getName(this: MyClass) {
return this.name;
}
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g());
The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
Try

この方法は、アロー関数アプローチの反対のトレードオフを行います。

  • JavaScript の呼び出し元は、それを認識せずにクラスメソッドを誤って使用している可能性があります。
  • クラスインスタンスごとに1つではなく、クラス定義ごとに1つの関数だけが割り当てられます。
  • 基底メソッドの定義は、super を介して呼び出すことができます。

this

クラスでは、this という特殊な型は、現在のクラスの型を動的に参照します。これがどのように役立つかを見てみましょう。

ts
class Box {
contents: string = "";
set(value: string) {
(method) Box.set(value: string): this
this.contents = value;
return this;
}
}
Try

ここで、TypeScript は set の戻り値の型が Box ではなく this であると推論しました。次に、Box のサブクラスを作成しましょう。

ts
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}
 
const a = new ClearableBox();
const b = a.set("hello");
const b: ClearableBox
Try

パラメータ型注釈で this を使用することもできます。

ts
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
Try

これは other: Box と記述するのとは異なります。派生クラスがある場合、その sameAs メソッドは、その同じ派生クラスの他のインスタンスのみを受け入れるようになります。

ts
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}
 
class DerivedBox extends Box {
otherContent: string = "?";
}
 
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.
Try

this に基づく型ガード

クラスとインターフェイスのメソッドの戻り位置に this is Type を使用できます。型絞り込み(例えば、if ステートメント)と組み合わせると、ターゲットオブジェクトの型は指定された Type に絞り込まれます。

ts
class FileSystemObject {
isFile(): this is FileRep {
return this instanceof FileRep;
}
isDirectory(): this is Directory {
return this instanceof Directory;
}
isNetworked(): this is Networked & this {
return this.networked;
}
constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
constructor(path: string, public content: string) {
super(path, false);
}
}
 
class Directory extends FileSystemObject {
children: FileSystemObject[];
}
 
interface Networked {
host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
fso.content;
const fso: FileRep
} else if (fso.isDirectory()) {
fso.children;
const fso: Directory
} else if (fso.isNetworked()) {
fso.host;
const fso: Networked & FileSystemObject
}
Try

このベースの型ガードの一般的なユースケースは、特定のフィールドの遅延検証を可能にすることです。例えば、このケースでは、hasValue が true であることが検証された場合、ボックス内に保持されている値から undefined が削除されます。

ts
class Box<T> {
value?: T;
 
hasValue(): this is { value: T } {
return this.value !== undefined;
}
}
 
const box = new Box<string>();
box.value = "Gameboy";
 
box.value;
(property) Box<string>.value?: string
 
if (box.hasValue()) {
box.value;
(property) value: string
}
Try

パラメータプロパティ

TypeScript は、コンストラクタパラメータを同じ名前と値を持つクラスプロパティに変換するための特別な構文を提供します。これらはパラメータプロパティと呼ばれ、コンストラクタ引数の先頭に可視性修飾子 publicprivateprotected、または readonly のいずれかを付けることで作成されます。結果のフィールドには、それらの修飾子が適用されます。

ts
class Params {
constructor(
public readonly x: number,
protected y: number,
private z: number
) {
// No body necessary
}
}
const a = new Params(1, 2, 3);
console.log(a.x);
(property) Params.x: number
console.log(a.z);
Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.
Try

クラス式

背景資料
クラス式 (MDN)

クラス式は、クラス宣言と非常によく似ています。唯一の実際の違いは、クラス式には名前が必要ないことですが、最終的にバインドされた識別子を介してそれらを参照できます。

ts
const someClass = class<Type> {
content: Type;
constructor(value: Type) {
this.content = value;
}
};
 
const m = new someClass("Hello, world");
const m: someClass<string>
Try

コンストラクタシグネチャ

JavaScript クラスは、new 演算子でインスタンス化されます。クラス自体の型が与えられた場合、InstanceType ユーティリティ型はこの操作をモデル化します。

ts
class Point {
createdAt: number;
x: number;
y: number
constructor(x: number, y: number) {
this.createdAt = Date.now()
this.x = x;
this.y = y;
}
}
type PointInstance = InstanceType<typeof Point>
 
function moveRight(point: PointInstance) {
point.x += 5;
}
 
const point = new Point(3, 4);
moveRight(point);
point.x; // => 8
Try

abstract クラスとメンバー

TypeScript のクラス、メソッド、およびフィールドは、abstract にすることができます。

抽象メソッドまたは抽象フィールドとは、実装が提供されていないもののことです。これらのメンバーは抽象クラスの中に存在する必要があり、抽象クラスは直接インスタンス化することはできません。

抽象クラスの役割は、すべての抽象メンバーを実装するサブクラスの基本クラスとして機能することです。クラスに抽象メンバーがない場合、そのクラスは具象であると言われます。

例を見てみましょう。

ts
abstract class Base {
abstract getName(): string;
 
printName() {
console.log("Hello, " + this.getName());
}
}
 
const b = new Base();
Cannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.
Try

Base は抽象クラスであるため、new でインスタンス化することはできません。代わりに、派生クラスを作成し、抽象メンバーを実装する必要があります。

ts
class Derived extends Base {
getName() {
return "world";
}
}
 
const d = new Derived();
d.printName();
Try

基本クラスの抽象メンバーを実装し忘れると、エラーが発生することに注意してください。

ts
class Derived extends Base {
Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.
// forgot to do anything
}
Try

抽象コンストラクトシグネチャ

場合によっては、抽象クラスから派生したクラスのインスタンスを生成するクラスコンストラクター関数を受け入れたいことがあります。

たとえば、次のようなコードを書きたいとします。

ts
function greet(ctor: typeof Base) {
const instance = new ctor();
Cannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.
instance.printName();
}
Try

TypeScript は、抽象クラスをインスタンス化しようとしていると正しく指摘しています。結局のところ、greet の定義が与えられれば、このコードを書くことは完全に合法であり、最終的に抽象クラスを構築することになります。

ts
// Bad!
greet(Base);
Try

代わりに、コンストラクトシグネチャを持つ何かを受け入れる関数を書きたいのです。

ts
function greet(ctor: new () => Base) {
const instance = new ctor();
instance.printName();
}
greet(Derived);
greet(Base);
Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.
Try

これで TypeScript は、どのクラスコンストラクター関数を呼び出すことができるかを正しく指摘します。Derived は具象なので呼び出すことができますが、Base はできません。

クラス間の関係

ほとんどの場合、TypeScript のクラスは、他の型と同じように構造的に比較されます。

たとえば、これら 2 つのクラスは同一であるため、互いに代用して使用できます。

ts
class Point1 {
x = 0;
y = 0;
}
 
class Point2 {
x = 0;
y = 0;
}
 
// OK
const p: Point1 = new Point2();
Try

同様に、明示的な継承がない場合でも、クラス間のサブタイプ関係が存在します。

ts
class Person {
name: string;
age: number;
}
 
class Employee {
name: string;
age: number;
salary: number;
}
 
// OK
const p: Person = new Employee();
Try

これは単純に聞こえますが、他のものよりも奇妙に見えるケースがいくつかあります。

空のクラスにはメンバーがありません。構造的な型システムでは、メンバーを持たない型は一般的に他のすべての型のスーパータイプです。したがって、空のクラスを記述した場合(記述しないでください!)、他のすべてをその代わりに使用できます。

ts
class Empty {}
 
function fn(x: Empty) {
// can't do anything with 'x', so I won't
}
 
// All OK!
fn(window);
fn({});
fn(fn);
Try

TypeScript ドキュメントはオープンソースプロジェクトです。これらのページの改善にご協力ください。プルリクエストを送信してください

このページの貢献者
RCRyan Cavanaugh (60)
OTOrta Therox (15)
HAHossein Ahmadian-Yazdi (6)
Uuid11 (2)
DSDamanjeet Singh (1)
21+

最終更新日: 2024年3月21日