NativeScriptの型不安全なObservableを型安全にする

 NativeScriptのObservableは、プロパティへの代入(っぽいメソッド)から、処理をフックすることができるクラスです。 MVVMパターンとかに使えます。 (って公式が言ってた。)

 しかしこのObservableにはやばい欠陥があって、上記リンクを見てもらえればわかるのですが、代入と取得(に対応するメソッド)の型が忘却されちゃってます。

export class Observable {
  // ...

  //                 vvv
  get(name: string): any

  //                       vvv
  set(name: string, value: any): void

  // ...
}

 え、any……? あっ、あたなは最低です! 静的型付けをなんだと思っているのですか!

 TypeScriptの型付けはめっちゃ考えられているので、型引数で情報を渡してあげるだけで、それに対する安全なget/setの型を導出してあげられます。 ただしsuperの最低なget/setをなかったことにはできないので、deprecatedにしておいて、新しくassign/takeというのを定義してあげることにします。 (このdeprecated-decoratorは、そのメソッドが実行時に使用された際に、メッセージをコンソールに出力するものなので、静的なものではないようです。注意。)

ヘルパー型さん field-contravariants

/**
 * Declare a type T of a property K via K of a type X.
 *
 * - Field<{foo: number, bar: string}, 'foo', number> = 'foo'
 * - Field<{foo: number, bar: string}, 'bar', string> = 'bar'
 * - Field<{foo: number, bar: string}, 'foo', string> = never
 */
type Field<X, K extends string, T> = K extends keyof X
  ? T extends X[K] ? K : never
  : never

冒涜的でないObservable take-null-possibility

/**
 * Don't dirty your hands.
 * You must use this instead of [[Untyped.Observable]].
 */
export default class Observable<X extends object> extends Untyped.Observable {
  constructor() {
    super()
  }

  /**
   * A typed safety set()
   */
  public assign<K extends string, T>(name: Field<X, K, T>, value: T): void {
    super.set(name, value)
  }

  @deprecated('assign')
  public set(name: string, value: any): void {
    super.set(name, value)
  }

  /**
   * A type safety get()
   */
  public take<K extends string, T>(key: Field<X, K, T>): T | undefined {
    return super.get(key)
  }

  @deprecated('take')
  public get(name: string): any {
    super.get(name)
  }
}

チェックしてみる。

const p = new Observable<{ x: number, y: string }>()
p.assign('x', 10)
p.assign('y', 'poi')
// 2345: Argument of type '"y"' is not assignable to parameter of type 'never'.
// p.assign('y', 10)

const x: number = p.take('x')
const y: string = p.take('y')

// 2345: Argument of type '"y"' is not assignable to parameter of type 'never'.
// const e: number = p.take('y')

OK!!!

 コード全文はこちら 👇

 TypeScriptのconditional typesは初めてだったので、いい型慣らし肩慣らしになったと思います。 TypeScriptに型を付けてもらうのではなく、TypeScriptで型を付けにいく側にまわった気分はどうだ?

 See you next time 👋


  1. ここでT extends X[K]なのは、const z = new Observable<{x: {xx: number}}>(); z.assign('x', {})を許さないためです。
  2. ここでtakeの戻り値がnullableなのは、const z = new Observable<{x: number}>; z.take('x')としたときにnullを返すためです。(Observableは必ずしも初期化を強制しない。)

筆者プロフィール

my-latest-logo

aiya000(あいや)

せつラボ 〜圏論の基本〜」 「せつラボ2~雲と天使と関手圏~」 「矢澤にこ先輩といっしょに代数!」を書いています!

強い静的型付けとテストを用いて、バグを防ぐのが好き。Haskell・TypeScript。