TypeScript
More than 1 year has passed since last update.

導入

世の中にはRamada.jsというものがあって。これはjsを書いている限りはなかなか便利な代物なんだが
TypeScriptと一緒に使うとなるとおよそ使う利点がなくなるほどに型を書く羽目になる。(ramdaの特性上@types/ramdaでは追いつかないところがあるので)

今回はその中の関数 whereあたりと遊んでみることにした。

where

where関数は`types/ramdaにある定義ではざっくりいうとこんな感じ

interface ObjFunc2 {
  [index: string]: (x: any, y: any) => boolean;
}
where<ObjFunc2>(spec: ObjFunc2): <U>(testObj: U) => boolean;

github

ただこの型の書き方の場合、挙動はわからない(おそらく意図的に緩めに型が書かれている)

x.js
const pred = where({
  a(x) {return x > 0},
  b(x) {return x > 0}
})

pred({
  a: 7,
  b: 8
}) //true
pred({
  a: -1,
  b: 8
}) //false

つまりそれっぽい関数のMapをわたせばそれっぽいを判定する関数を返してくれるということなのだ。

雑な実装の場合、以下のようになる

where.js
function where(fs) {
    return function (x) {
        for (const key in fs) {
            if (!fs[key](x[key])) {
                return false
            }
        }
        return true;
    }
}

tsにはtype predicate function (独自日本語訳is系関数)と呼ばれがちな便利な関数がある。

説明を省くと、where関数がis系関数になりうる場合がある。

その時にいい感じに型がついてほしい。

その欲求を満たす型がこれ

type Where<Table> = {
    [P in keyof Table]: (x: any) => x is Table[P]
}
function where<T extends object>(fs: Where<T>) {
    return function (x: any): x is T {
        for (const key in fs) {
            if (!fs[key](x[key])) {
                return false
            }
        }
        return true;
    }
}

is

注意したいのが、is系関数はあくまでbooleanを返す関数で、こっちが勝手に「これはこの型なんだ」って言い張るので、実装付きの型キャストに近い。

つまり実装者の知能がまともであれば、どう実装しても良い。

たとえば isString関数は

function isString(x: any): x is string {
   return typeof x === 'string'
}

こんな感じに実装できる。

先ほど説明したが実装者次第なので、例えば

実装者がコンパイルエラーが出るたびginを1shot決めるゲームを一人で遊んでおり、そのゲームも終盤にさしかかって以下のコードを書いたとしてもコンパイルは通ってしまう。

function isString(x: any): x is string {
   return typeof x !== 'string'
}

おおよその場合では、コード中で

if (typeof x === 'string') {
// some x matter
}

というを書くと思うが、上記のwhereでは関数として必要なのである。

isHogeを大量に書いても良いが、面白さにはかけるので

type PrimitiveRecord = {
    string: string;
    number: number;
    boolean: boolean;
    undefined: undefined;
    object: object | null;
    symbol: symbol;
    function: Function
}

export function is<K extends keyof PrimitiveRecord>(kind: K): (x: any) => x is PrimitiveRecord[K] {
    return function (x: any): x is PrimitiveRecord[K] {
        return typeof x === kind;
    }
}

こんな感じに書いてしまう。

こうすると is('string')とかできて便利。

とはいえ、string | numberをpredicateしたくなる人間がいるとは思うので、そういうやつにはこれを渡しておく

export function union<X, Y>(f: TypePred<X>, g: TypePred<Y>): TypePred<X | Y> {
    return function (z: any): z is X | Y {
        return f(z) || g(z);
    }
}

こうすると、人間はunion(is('string'), is('number'))と勝手にやってくれるので便利だ。(これは (x:any) => x is string | number)

実はこのPrimitiveRecordという名前と型をもうちょっと汎用的に使いたい時がでてくる。

type Point = {
    x: number
    y: number
}

type Record = {
    Point: Point
    string: string;
    number: number;
    boolean: boolean;
}

こうした時に is('Point')でis関数が作れると便利かもしれない(特にコード自動生成のとき)

そんなときにはこのgenIs関数を作れば解決する。

function genIs<Table>(mapper: Where<Table>) {
    return function is<K extends keyof Table>(kind: K) {
        return function (x: any): x is Table[K] {
            if (mapper[kind](x)) {
                return true
            }
            return false
        }
    }
}

const myis = genIs({
    string: is('string'),
    number: is('number'),
    boolean: is('boolean'),
    Point: where({
        x: is('number'),
        y: is('number')
    })
})

myis('Point') // (x: any) => x is Point

気づいている人もいるかもしれないが、is関数を使って上のis関数を作れる

const is2 = genIs({
    string: is('string'),
    number: is('number'),
    boolean: is('boolean'),
})

const is3 = genIs({
    string: is2('string'),
    number: is2('number'),
    boolean: is2('boolean'),
})

ちなみに?を使ってoptionalなpropertyを作った時に若干おかしくなるので、少し型の詰めが甘い
keyofを使った時にoptionalなkeyという概念が中で存在しているせいだと思われる。

type Point2 = {
    x: number
    y?: number
}

type Point3 = {
    x: number
    y: number | undefined
}

とかで試してもらうとわかる