最近、私のホームプロジェクトの不適切な動作の理由を研究していると、疲労のためにしばしば繰り返される間違いに再び気付きました。 エラーの本質は、コードの1つのブロックに複数の識別子があり、関数を呼び出すときに、別のタイプのオブジェクトの識別子を渡すことです。 この記事では、TypeScriptを使用してこの問題を解決する方法について説明します。
理論のビット
TypeScriptは構造型に基づいており、JavaScriptのカモのイデオロギーによく適合しています。 これについて十分な数の記事が書かれています。 それらを繰り返すことはせず、他の言語でより一般的である主格タイピングとの主な違いのみを概説します。 小さな例を見てみましょう。
class Car { id: number; numberOfWheels: number; move (x: number, y: number) { // } } class Boat { id: number; move (x: number, y: number) { // } } let car: Car = new Boat(); // TypeScript let boat: Boat = new Car(); //
TypeScriptがこのように動作するのはなぜですか? これは、構造タイピングの現れです。 型名を監視する主格とは異なり、構造型指定は、その内容に基づいて型の互換性を決定します。 Carクラスには、Boatクラスのすべてのプロパティとメソッドが含まれているため、CarをBoatとして使用できます。 ボートにはnumberOfWheelsプロパティがないため、逆は当てはまりません。
識別子の入力
最初に、識別子のタイプを設定します
type CarId: number; type BoatId: number;
これらの型を使用してクラスを書き換えます。
class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) { // } } class Boat { id: BoatId; move (x: number, y: number) { // } }
状況をあまり変えていないことに気付くでしょう。なぜなら、私たちはまだ識別子をどこから取得したかを制御できないからです。あなたは正しいでしょう。 しかし、この例にはすでにいくつかの利点があります。
プログラムの開発中に、識別子のタイプが突然変更される場合があります。 そのため、たとえば、プロジェクトに固有の特定の自動車番号を文字列VIN番号に置き換えることができます。 識別子のタイプを指定せずに、発生するすべての場所で数値を文字列に置き換える必要があります。 タイプのタスクでは、タイプ自体が決定される1つの場所でのみ変更を行う必要があります。
関数を呼び出すと、コードエディターから、型識別子がどうあるべきかというヒントが得られます。 次の関数が宣言されているとします:
function getCarById(id: CarId): Car { // ... } function getBoatById(id: BoatId): Boat { // ... }
その後、エディターから、数字だけでなくCarIdまたはBoatIdを送信する必要があるというヒントを取得します。
最も厳しいタイピングをエミュレートする
TypeScriptには名目上の型付けはありませんが、その動作をエミュレートして、任意の型を一意にすることができます。 これを行うには、一意のプロパティをタイプに追加する必要があります。 このトリックは、ブランディングという用語で英語の記事に記載されており、次のようになります。
type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'};
私たちの型は、数値と一意の値を持つプロパティを持つオブジェクトの両方であるべきだと指摘したので、構造型を理解する上で型の互換性をなくしました。 仕組みを見てみましょう。
let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId); // OK car = getCarById(boatId); // ERROR boat = getBoatById(boatId); // OK boat = getBoatById(carId); // ERROR carId = 1; // ERROR boatId = 2; // ERROR car = getCarById(3); // ERROR boat = getBoatById(4); // ERROR
最後の4行を除いて、すべてが正常に見えます。 識別子を作成するには、ヘルパー関数が必要です。
function makeCarIdFromVin(id: number): CarId { return vin as any; }
この方法の欠点は、この関数が実行時に残ることです。
厳密な型付けをもう少し厳しくする
最後の例では、追加の関数を使用して識別子を作成する必要がありました。 Flavorインターフェース定義を使用して削除できます。
interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;
これで、次のように識別子のタイプを設定できます。
type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”>
_typeプロパティはオプションであるため、暗黙的な変換を使用できます。
let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK
そして、まだ識別子を混同することはできません:
let carId: CarId = boatId; // ERROR
選択するオプション
両方のオプションには存在する権利があります。 ブランディングには、変数を直接割り当てから保護するという利点があります。 これは、変数が絶対ファイルパス、日付、IPアドレスなどの何らかの形式で文字列を保存する場合に役立ちます。 この場合の型変換を処理するヘルパー関数は、入力データをチェックおよび処理することもできます。 それ以外の場合、Flavorを使用する方が便利です。