Decoratorを使用したプロパティ型の強制強制


この記事では、デコレータ関数を使用して角度成分が入力型の広い範囲を受け入れますが、厳密に内部の型に透過的に変換する方法を示します.内部データ型の厳密性を保証しつつ、コンポーネントAPIをより柔軟にしたい場合に便利です.
記事のソースコード全体を表示できますGitHub .

デコレータは何ですか。


JavaScriptデコレータは、クラス、メソッド、およびプロパティの既定の動作を変更する関数です.Javaなどの他のプログラミング言語と同様に、CのChorkとPythonは、透過的に私たちのコードのさまざまな側面を強化するためにそれらを使用することができます.アングル、Litt、AureliaのようなWeb UIフレームワークは、コンポーネントモデルのビルディングブロックとして使用します.ノード.また、NestjsやSequelize TypescriptとTypeOrmのようなJSフレームワークやライブラリも、そのAPIをより表現力豊かにするために、デコレーターを提供します.デコレータの使用に関するスポットの例は、typeOrmのデータベース実体宣言です.
// example taken from the official documentation of TypeORM
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Photo {
  @PrimaryGeneratedColumn()
  id: number

  @Column({
    length: 100,
  })
  name: string

  @Column("text")
  description: string

  @Column()
  filename: string

  @Column("double")
  views: number

  @Column()
  isPublished: boolean
}
The Entity , PrimaryGeneratedColumn and Column Decoratorは、プレーンJavaScriptクラスを特定の列特性を持つデータベース表にマップされたエンティティに変換します.何が最も印象的ですか、我々は全く手続き上のコードなしでこのすべてを成し遂げます.テーブル定義は、それを読んで、理解しやすくするために宣言です.すべての複雑な手続きの指示は、デコレーターの機能自体の中に、私たちの目から隠されている.ケアと思考で設計されたデコレータは、上記のようにエレガントなAPIとして作成することができます.
JavaScriptランタイムではまだデコレータをネイティブにサポートしていませんが、同じ結果を得るためにトランスポーズを使用する実装があります.最も中古品は@babel/plugin-proposal-decorators and TypeScript's experimental decorators . 2022年3月の終わりに、デコレーターの提案がステージ3に達したので、我々は彼らがかなりすぐにecmascript仕様の公式部分になることを期待することができます.私は、デコレータが彼らの現在の状態で探検する価値があると思います.最悪のシナリオでは、JavaScriptコミュニティが移行戦略を定義するのを待っている間、ポリフィルを使用し続けることができます.
この記事では、私はあなたからの方法を使用してexperimentalDecorators コンパイラフラグはすべての角プロジェクトでデフォルトでアクティブです.

なぜ我々は角度成分で型強制を必要とするのか?


あなたはおそらく聞いた"type coercion" JavaScriptエンジンの暗黙のうち、データ型の変換を行うには、
  • 1 + "2" === "12"
  • true + 1 === 2
  • [] / 1 === 0
  • (!null === !undefined) === true
  • つのタイプから他の原因へのこの自動変換は、多くの未熟な開発者に頭痛を引き起こします.自分自身を考慮した人は、どんなコストでも暗黙のタイプ変換を避けるようにあなたに言います.私はあなたがどのように動作し、あなたの利点にその知識を使用する方法を学ぶ必要がありますと言う.角成分入力プロパティの型強制をどのように適用できるかを見てみましょう.
    私たちには、次の角成分があると想像してください.
    @Component({
      selector: "my-counter",
      template: `
        <button (click)="decrement()" [disabled]="disabled">-</button>
        <span>{{ count }}</span>
        <button (click)="increment()" [disabled]="disabled">+</button>
      `,
    })
    export class CounterComponent {
      @Input()
      disabled = false
    
      @Input()
      count = 0
    
      increment() {
        this.count++
      }
    
      decrement() {
        this.count--
      }
    }
    
    ここに2つある@Input s
  • disabled ユーザーが数を変更できるかどうかを制御する
  • count 数の初期値
  • このような角度テンプレートでコンポーネントを使用できます.
    <my-counter [count]="42" [disabled]="true"></my-counter>
    
    テンプレートは、すべての開発者に対して角度の背景を持っているように見えますが、時々バニラHTMLやWebコンポーネントに熟練したチームメンバーがいるかもしれません.我々は、Webコンポーネントで主に製品開発の仕事から、チームメイトの角度でチームのコンポーネントを開発している想像してください.現在、上部管理は、新製品のためにプロトタイプを緊急に構築することを彼らに要求しました.そのような状況では、ネイティブのHTMLとWebコンポーネントがどのように動作するかを模倣する、より柔軟で寛容なAPIが欲しいかもしれません.
    <!-- count === 42, disabled === true -->
    <my-counter count="42" disabled="true"></my-counter>
    
    <!-- count === 42, disabled === false -->
    <my-counter count="42" disabled="false"></my-counter>
    
    <!-- disabled === true -->
    <my-counter disabled></my-counter>
    
    このAPIは、角度特有のプロパティ束縛構文に関連する複雑さを隠します、そして、すべては我々のチームメイトのために直観的に働きます.我々はコンポーネントの作者としてBabysitは、製品開発者とは、彼らはすでによく知っているものとの類似性によって権限を感じる必要があります.
    しかし、コンポーネントの現在の状態ではできません.プロジェクト設定によって2つの失望結果を得ることができます.
  • 文字列を受信しますcount and disabled を返します.これはバグや予期しないコンポーネントの動作を診断するのを難しくする可能性があります.
  • 我々がそうするならば、我々のコードはコンパイルしませんstrictTemplates コンパイラフラグがオンになった.コンパイラは、予期した型をコンポーネント入力に渡していないと文句を言います.
  • これらのどちらも、我々が望む何かです.我々は、すべてがちょうど働くことを望みます™. この問題はとても一般的です、角度チームはCDK(コンポーネント開発キット)でそれのためにデフォルト解決を含みました.我々は、インポートすることができます@angular/cdk/coercion パッケージを使用して別の強制的なユーティリティ関数を使用します.このアプローチには、いくつかの警告があります.
  • 単純なパブリックプロパティを、それぞれのフィールドをバッキングして、ゲッター/セッターペアに変えなければなりません
  • 厳密なテンプレートを使用している場合、別の入力と内部の型を使用することをコンパイラに知らせるために、受け入れられた入力型を個別に宣言する必要があります
  • アクションでこれを見ましょう.
    // Note: irrelevant code skipped for brevity.
    import {
      coerceBooleanProperty,
      BooleanInput,
      NumberInput,
      coerceNumberProperty,
    } from "@angular/cdk/coercion"
    
    export class Counter {
      // static properties prefixed with "ngAcceptInputType_"
      // tell the compiler figure what is the real input type
      static ngAcceptInputType_disabled: BooleanInput
      static ngAcceptInputType_count: NumberInput
    
      @Input()
      get disabled() {
        return this._disabled
      }
      set disabled(value: boolean) {
        this._disabled = coerceBooleanProperty(value)
      }
      private _disabled = false
    
      @Input()
      get count() {
        return this._count
      }
      set count(value: number) {
        this._count = coerceNumberProperty(value, 0)
      }
      private _count = 0
    }
    
    私たちを強制するコードの6行について@Input プロパティとこれは最も簡単なケースです.正しいテンプレート型推論に必要な静的フィールドをカウントしていません.コンパイラチェックを無効にすることなく、この辺で動作することはできません.すべてのコンポーネントでそのような入力の数によって型強制に必要な行を乗算すると、Boilerplateコードの合計サイズが劇的に増加します.すべてのこの論理を6行の代わりに1行のコードで表現する方法を考えることができますか?
    export class CounterComponent {
      static ngAcceptInputType_disabled: BooleanInput
      static ngAcceptInputType_count: NumberInput
    
      @OfTypeBoolean()
      @Input()
      disabled = false
    
      @OfTypeNumber()
      @Input()
      count = 0
    }
    
    あなたが正しく推測-これはプロパティのデコレータの理想的なユースケースです.型強制論理をデコレータ関数に抽出することにより,このようなboilerplateコードを部品から取り除くことができる.

    型強制概念デコレータの作成


    基本的なプロパティを、関連するプライベートフィールドを持つゲッター/セッターペアに変換できるプロパティデコレータ機能を設計しましょう.最も簡単なものはBoolean型でなければなりません.
    // of-type-boolean.decorator.ts
    import { coerceBooleanProperty } from "@angular/cdk/coercion"
    
    export function OfTypeBoolean() {
      return function decorator(target: unknown, propertyKey: PropertyKey): any {
        const privateFieldName = `_${String(propertyKey)}`
    
        Object.defineProperty(target, privateFieldName, {
          configurable: true,
          writable: true,
        })
    
        return {
          get() {
            return this[privateFieldName]
          },
          set(value: unknown) {
            this[privateFieldName] = coerceBooleanProperty(value)
          },
        }
      }
    }
    
    export type BooleanInputType = "" | "true" | "false" | boolean
    
    このコードは次のように動作します.
  • プロパティの値を格納するアンダースコアをプレフィックスしたフィールドを定義します.
  • このフィールドを公開し、セッターでブールにそれを強制するゲッター/セッター対を定義します.
  • 静的コンポーネントの角コンポーネントで使用するカスタム型を作成するngAcceptInputType フィールド.
  • 使用に注意してくださいthis このメソッドでは、現在のコンポーネントのインスタンスを参照します.それは、使用する誘惑ですtarget でもここからは間違いだろうtarget はコンポーネントのプロトタイプです.つまり、文脈の中でget() 機能Object.getPrototypeOf(this) === targettrue .
    同じデコレータを作りましょう.
    // of-type-number.decorator.ts
    import { coerceNumberProperty } from "@angular/cdk/coercion"
    
    export function OfTypeNumber() {
      return function decorator(target: unknown, propertyKey: PropertyKey): any {
        const privateFieldName = `_${String(propertyKey)}`
    
        Object.defineProperty(target, privateFieldName, {
          configurable: true,
          writable: true,
        })
    
        return {
          get() {
            return this[privateFieldName]
          },
          set(value: unknown) {
            this[privateFieldName] = coerceNumberProperty(value)
          },
        }
      }
    }
    
    export type NumberInputType = number | string
    
    見てわかるように、違いは、保磁力関数の1行と入力型宣言の1行です.我々はさらに一歩を踏み出すことができ、工場の機能に共通のパターンを抽出します.これにより、将来的に新しい型強制型デコレータを作成することも容易になります.

    強制的なデコレータファクトリ関数の作成


    次のようにすべての強制的な装飾子の繰り返し論理を抽象化しましょう.
    // coercion-decorator-factory.ts
    export function coercionDecoratorFactory<ReturnType>(
      coercionFunc: (value: unknown) => ReturnType
    ) {
      return function (target: unknown, propertyKey: PropertyKey): any {
        const privateFieldName = `_${String(propertyKey)}`
    
        Object.defineProperty(target, privateFieldName, {
          configurable: true,
          writable: true,
        })
    
        return {
          get() {
            return this[privateFieldName]
          },
          set(value: unknown) {
            this[privateFieldName] = coercionFunc(value)
          },
        }
      }
    }
    
    強制的に関数を渡すことができます.また、一般的な引数として強制関数の戻り値の型を指定する必要があります.これは、予期された型を失敗しないようにするための健全性チェックです.
    では、このデコレータファクトリを使用して解析用の新しいデコレータを構築しましょうDate オブジェクト.その目的はISO 8601文字列、タイムスタンプ(両方の数と文字列)ともちろん、日付を受け入れることです.Date インスタンス.その結果、入力引数をDate , サポートされている形式に関係なく:
    // of-type-date.decorator.ts
    import { coercionDecoratorFactory } from "./coercion-decorator-factory"
    
    export function OfTypeDate() {
      return coercionDecoratorFactory<Date>((date: unknown) => {
        // that's pretty naive parsing,
        // please, don't use it in production!
        if (date instanceof Date) {
          return date
        } else if (typeof date === "string") {
          if (Number.isInteger(Number(date))) {
            return new Date(Number(date))
          }
    
          return new Date(Date.parse(date))
        } else if (typeof date === "number") {
          return new Date(date)
        }
    
        throw Error(`The value ${date} can't be converted to Date!`)
      })
    }
    
    export type DateInputType = string | number | Date
    
    次に、日付強制型デコレータを短い日付をレンダリングするコンポーネント(時刻情報なし)に統合します.
    // short-date.component.ts
    import { Component, Input } from "@angular/core"
    import { DateInputType, OfTypeDate } from "./decorators/of-type-date.decorator"
    
    @Component({
      selector: "my-short-date",
      template: `{{ date | date: "shortDate" }}`,
    })
    export class ShortDateComponent {
      static ngAcceptInputType_date: DateInputType
    
      @OfTypeDate()
      @Input()
      date: Date | undefined
    }
    
    以下のように使用できます:
    <!-- 04/08/22 -->
    <my-short-date date="2022-04-08T19:30:00.000Z"></my-short-date>
    
    <!-- 01/01/00 -->
    <my-short-date date="946677600000"></my-short-date>
    <my-short-date [date]="946677600000"></my-short-date>
    
    <!-- whatever the value of the bound `dateOfBirth` property is -->
    <my-short-date [date]="dateOfBirth"></my-short-date>
    
    あなたが見ることができるように、この構成要素は使いやすくて、不正確なユーザー入力により弾力があります.

    結論


    コード複製を減らすためにデコレータを使用し、有用な動作で角度成分を強化することができます.デコレータは、両方の開発者の経験と当社のコンポーネントのビジネスロジックの正しさを向上させることができます.すべてのこれらの利点は、我々のコードベースに多くの雑音と複雑さを加えない宣言式の形で来ます.
    角度ランタイム、そのテンプレートコンパイラ、typescript、およびこれらの間の緊密な統合の複雑さのために、この環境でのメタプログラミングは、醜いハッキングと回避に頼る必要があるかもしれません.だからこそ、UIエンジニアは常に開発者の経験、コードの品質と機能の間に適切なバランスを保つ必要があります.
    あなたはこのデモの完全なソースコードを得ることができますGitHub .
    この記事は、JavaScriptのデコレータの興味深いユースケースをあなたのプロジェクトに統合することを考えていることを示唆しています!