タイプスクリプトにおけるオブジェクト指向プログラミングの原理


オブジェクト指向プログラミング(OOP)はJavaScriptのようなダイナミックなプロトタイプ言語で達成するのが難しいです.アヒルの入力のような言語機能のために手動でOOPの原則に固執する必要があります.これには規律が必要である.異なる背景を持つ開発者の多様なチームが関与している場合、良い意図で満たされたコードベースはすぐに1つのカオス混乱になることができます.
このテイクでは、我々は、言語が手動労働の束を自動化し、最高のプラクティスを奨励する方法を示す、typescriptの適切なOOP技術を掘ります.アヒルのタイピングについて少し話をし、3つの柱に入る.カプセル化、継承、多形.
レディ?レッツゴー!

タイプスクリプトのアヒルタイピングのビット


あなたは、コピーペーストコードによってTypeScript Playground . 目標は、これらのテクニックを任意のコードベースで動作することを証明するために、次の再現することです.
以下を見てください.
interface Todo {
  title: string;
  description?: string;
}

const todo1 = {
  title: "organize desk",
  extra: "metadata", // duck typing is allowed!
};

const updateTodo = (
  todo: Todo,
  fieldsToUpdate: Partial<Todo> // allow partial updates
) => ({ ...todo, ...fieldsToUpdate });

const result1 = updateTodo(todo1, {
  description: "throw out trash",
});

const todo2 = {
  ...todo1,
  description: "clean up", // call bombs without description
};

const updateRequiredTodo = (
  todo: Required<Todo>,
  fieldsToUpdate: Partial<Todo>
): Required<Todo> => ({ ...todo, ...fieldsToUpdate });

const result2 = updateRequiredTodo(todo2, {
  description: "throw out trash",
});
The Todo インターフェイスはオプションのプロパティで宣言されますdescription , したがって、このコードはプロパティをスキップできます.疑問符? これがオプションであることをタイプスクリプトに伝えます.このデザイン決定を取り戻す一つの方法は、インタフェースをラップすることですRequired<Todo> , これはすべてのプロパティを非オプションにします.古典的なOOPでは、オブジェクトのデータ完全性は重要です.ここで、コンパイラはこの動作を自動化します.
意図が部分更新を許すならば、Partial<Todo> オプションのプロパティに戻すことができます.これはパラメータfieldsToUpdate . The updateRequiredTodo 関数は明示的にRequired<Todo> , 返されるオブジェクトの形状を保証します.
プロパティに注意してくださいextra を受け入れるtodo1 . これは、タイプスクリプトがJavaScriptのスーパーセットであるという事実からの遺産です、そして、アヒル入力は許されます.明示的なタイピングがなければ、typescriptにはレガシーに戻るしかない.より多くのタイプスクリプトプログラムを書くので、明示的な型を通して可能な限りタイプチェッカーに頼るのは良い考えです.
さて、オブジェクト指向プログラミングの3つの柱を見てみましょう.

タイプスクリプトによるオブジェクト指向プログラミングの三つの柱


カプセル化


この柱はすべてソフトウェアのモジュールへのアクセスとデカップリングを制限することです.TypeScriptは、この情報を非表示のテクニックをクラスを介して達成することができます.
class Base {
  private hiddenA = 0;
  #hiddenB = 0;

  printInternals() {
    console.log(this.hiddenA); // works
    console.log(this.#hiddenB); // works
  }
}

const obj = new Base();
console.log(obj.hiddenA); // these two bomb
console.log(obj.#hiddenB);
ここでhiddenA メンバーのアクセス制限Base コンパイラによって実施されます.しかし、コンパイラはJavaScriptランタイム構造体を与えますin 隠しプロパティへのアクセス.あなたは、ハードプライベートを使用することができます# プライベートフィールドを維持する.
クラスは、あなたのタイプスクリプトアーセナルで利用できる唯一のツールでありません.ユーティリティタイプPick<Type, Keys> , Omit<Type, Keys> , Readonly<Type> , and NonNullable<Type> アクセスを制限することもできます.
interface Todo {
  title: string;
  description?: string; // string | undefined
  completed: boolean;
}

type TodoPreview1 = Pick<Todo, "title" | "completed">;

const todo1: TodoPreview1 = {
  //explicit typing
  title: "Clean room",
  completed: false,
  description: "x", // duck typing is NOT allowed
};

type TodoPreview2 = Omit<Todo, "description" | "completed">;

const todo2: TodoPreview2 = {
  title: "Clean room",
};

const todo3: Readonly<Todo> = {
  title: "Delete inactive users",
  completed: true,
};

todo3.completed = false; // bombs

const todo4: Todo = {
  ...todo1,
  description: "Doing shores is fun",
};

const description: NonNullable<string | undefined> =
  // bombs without null coalescing
  todo4.description ?? "";
The Pick and Omit ユーティリティ型はプロパティのサブセットをパックします.オブジェクトが明示的に入力された場合、アヒル入力は許可されません.このテクニックは、スライスとdicesタイプとコンパイラに利用可能な情報を狭くします.
不変性はReadonly . オブジェクトを突然変異しようとすると、自動的にビルドが失敗します.あなたがランタイムで保護を必要とするならば、Object.freeze も利用可能です.状態を変異できるオブジェクトを制限することで、カプセル化を強制し、型間の相互作用を保護できます.
あなたが仮定したかもしれないこの時間undefined or null null可能な型が許可されているので、OOPで動作するのは当然の結果です.まあ、許容可能なオブジェクトを提供する通りマンゴーを販売するようにして、あなたは決して運ぶことを意図した人々を伝えるようなものです!
エーnull したがって、オブジェクトの不在は、したがって、オブジェクトオリエンテーションに直径的に反対である.NonNullable あなたが予期しない災難がないように、タイプについて正直であるのを援助します.NULLが合体しない場合、コンパイラはあなたに吠えるでしょうdescription はオプションではない.

遺産


継承は、IS - A関係を持つ型の階層を表します.このテクニックは現実世界の関係を反映することができます.pingメソッドでpingできるインターフェースがあると言います.sonarがpingableであるためには、この動作を実装しなければなりません.これはソナーについての推論を容易にするので、特定の機能をモデル化します.
interface Pingable {
  ping(): void;
}

class Sonar implements Pingable {
  ping() {
    console.log("sonar ping!");
  }
}
TypeScriptも継承を達成するfancier方法が付属します.与えられた2つのタイプColorful and Circle , あなたは結合のプロパティと交差点を介して興味深い方法でセットのプロパティを組み合わせることができます.
union型では、型述語を通してナローイングが必要です.The circle is Circle 述語はそれに応じて分岐することができるように型を狭くします.コンパイラは、どのような種類の分離から選ぶかわからないので、このナローイング技術を必要とします.
type Colorful = {
  color: string;
};

type Circle = {
  radius: number;
};

type ColorfulCircle = Colorful | Circle; // union

function isCircle(circle: ColorfulCircle): circle is Circle {
  return "radius" in circle;
}

function draw(circle: ColorfulCircle) {
  if (isCircle(circle)) {
    // branch logic
    console.log(`Radius was ${circle.radius}`); // ok
  } else {
    console.log(`Color was ${circle.color}`);
  }
}

draw({ color: "blue" });
draw({ radius: 42 });
下記Colorful and Circle 両方からプロパティを持つ新しい型を作成するために交差します.これは古典的な継承のように、コードの再利用を促進します.
type Colorful = {
  color: string;
};

type Circle = {
  radius: number;
};

type ColorfulCircle = Colorful & Circle; // intersection

function draw(circle: ColorfulCircle) {
  console.log(`Radius was ${circle.radius}`); // ok
  console.log(`Color was ${circle.color}`);
}

draw({ color: "blue", radius: 42 });
The this JavaScriptのオブジェクトは、コンテキストによって変更されるので動作するようになります.型式this クラスに動的に型ガードを持っています.
abstract class Box {
  content: string = "";

  sameAs(other: this) {
    return other.content === this.content;
  }

  isDerivedBox(): this is DerivedBox {
    return this instanceof DerivedBox;
  }
}

class DerivedBox extends Box {
  otherContent: string = "?";
}

const base = new Box(); // bombs
const derived = new DerivedBox();
derived.isDerivedBox(); // true
derived.sameAs(derived as Box); // bombs
注意this is DerivedBox 返り値isDerivedBox メソッド.混合this 経由で型狭まりinstanceof 作りthis 移動目標の代わりに予測可能型.
このテクニックは、消費するコードが実装の詳細を無視することができる合理的な動作で、具体的なOOPタイプを作成します.The Box クラスも抽象的であり、コンパイラはクラスのインスタンスを許可しないことでアクセスを制限します.リッチな機能のいくつかを再利用したいかもしれない具体的なオブジェクトからの共通の振舞いとの契約.

オブジェクト指向プログラミングにおける多形


型によって異なる動作をする引数を使用して、アドホックな多型を実現できます.を見ましょうadd 一般的に動作し、それに応じて動作を変更するメソッドです.
interface GenericAdd<AddType> {
  add: (x: AddType, y: AddType) => AddType;
}

class GenericNumber implements GenericAdd<number> {
  add(x: number, y: number) {
    return x + y;
  } // number + number
}

class GenericString implements GenericAdd<string> {
  add(x: string, y: string) {
    return x + y;
  } // string + string
}

const genericNumber = new GenericNumber();
genericNumber.add(1, 2); // 3

const genericString = new GenericString();
genericString.add("Hello", ", Mammals!"); // Hello, Mammals!
型をレコードとして考えると、型のセクションだけで動作するコードを使用して、行の多型レコードを持つことができます.TypesScriptは、この簡単にPartial . 注意subset は明示的に入力され、betaAType .
type AType = { x: number; y: number; z: number };

const subset: Partial<AType> = {
  x: 2,
  y: 3,
  beta: "bomb", // not allowed
};
πθce de r抵抗に対し,組成とデコレータパターンを用いてliskov置換原理を適用した.ランタイムでは、オブジェクトは多形動作を介して機能を取得します.
あると言うBarista 種類のコーヒーを作りたいクラス.ここでの関係は、Barista Hasがコーヒーのカップであるということです.コーヒーのように、準備されているオブジェクトはミルク、砂糖、または振りかけることができます.私のBaristaは忙しくて、畳み込まれた嘆かわしいコードのために時間がないので、すべては再利用できて、使いやすいです.
interface Coffee {
  getCost(): number;
  getIngredients(): string;
}

class SimpleCoffee implements Coffee {
  getCost() {
    return 8;
  }

  getIngredients() {
    return "Coffee";
  }
}

abstract class CoffeeDecorator implements Coffee {
  constructor(private readonly decoratedCoffee: Coffee) {}

  getCost() {
    return this.decoratedCoffee.getCost();
  }

  getIngredients() {
    return this.decoratedCoffee.getIngredients();
  }
}

class WithMilk extends CoffeeDecorator {
  constructor(private readonly c: Coffee) {
    super(c);
  }

  getCost() {
    return super.getCost() + 2.5;
  }

  getIngredients() {
    return super.getIngredients() + ", Milk";
  }
}

class WithSprinkles extends CoffeeDecorator {
  constructor(private readonly c: Coffee) {
    super(c);
  }

  getCost() {
    return super.getCost() + 1.7;
  }

  getIngredients() {
    return super.getIngredients() + ", Sprinkles";
  }
}

class WithSugar extends CoffeeDecorator {
  constructor(private readonly c: Coffee) {
    super(c);
  }

  getCost() {
    return super.getCost() + 1;
  }

  getIngredients() {
    return super.getIngredients() + ", Sugar";
  }
}
The Coffee インターフェイスは、コード全体を一貫して使用し、現実世界のオブジェクトのモデルのように動作します.例えば、コーヒーにはいくつかの成分があり、お金がかかる.開始価格は、米国の通貨を想定し、インフレを調整するSimpleCoffee .
デコレータパターンはliskov置換原理の良い例ですCoffeeDecorator この同じ契約に固執する.これは、コードをより予測し、直感的になります.typescriptはサブクラスが契約を実装するのを保証する良い仕事をするので、開発者が奇妙な場所で奇妙な振舞いをくつがえすのは難しいです.
class Barista {
  constructor(private readonly cupOfCoffee: Coffee) {}

  orders() {
    this.orderUp(this.cupOfCoffee);
    let cup: Coffee = new WithMilk(this.cupOfCoffee);
    this.orderUp(cup);
    cup = new WithSugar(cup);
    this.orderUp(cup);
    cup = new WithSprinkles(cup);
    this.orderUp(cup);
  }

  private orderUp(c: Coffee) {
    console.log(
      "Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients()
    );
  }
}

const barista = new Barista(new SimpleCoffee());
barista.orders();
示されるように、消費するコードは使いやすくて、多くの複雑さを麻痺させます.これはOOPの究極の目標です:抽象的にすべての問題を離れて.だから私のBaristaは、単純なコーヒーを作るために低レベルのコードを介してポーリング時間を無駄にしない.
ボーナスとして、このコードは今ではテスト可能ですBarista クラスは同じ契約に固執する.あなたが実装する模擬を注入することができますCoffee Unitこのコードの全てをテストします.

ラップアップ:タイプスクリプトにおける適切なオブジェクト指向プログラミング技術の使用


このポストでは、オブジェクト指向プログラミングの3つの柱-カプセル化、継承、および多型を走らせました.
あなたは、タイプスクリプトがどのように最高の実行を自動化するかについて見ました.
これは、OOPの原則に固執するだけでなく、コードのにおいを取り除くことができます.あなたのタイプとコンパイラはあなたのコードをきれいにして、不幸な事故から自由にしておくために連合国になるべきです.
ハッピーコーディング!
あなたがこのポストが好きであるならば.subscribe to our JavaScript Sorcery list よりマジカルなJavaScriptのヒントやトリックに毎月の深いダイビングのため.
P . P . S .あなたのノードにAPMが必要な場合.JSアプリcheck out the AppSignal APM for Node.js .