なぽろぐ

気ままに感じたことを記事にまとめます。Vtuberのイベントのことと、プログラム関連のことが多めだと思います。

TypeScriptのTemplateStringTypesでorder関数

Array.prototype.sortにわたすorder関数の型定義

Playgroundはこちらいろいろ触ってみてください。

www.typescriptlang.org

文章

sortにわたすorder関数を配列の要素がネストしているオブジェクトだったとき即時関数で書かなきゃいけないのが辛いというのが根底にある気持ちです。

僕が求めるorder関数は、型とsortしたいkey(string)をわたしてdesc, ascだけ決めればokみたいなインターフェースでした。

TypeScriptだと配列の要素がネストしているオブジェクトのときkeyがstringでかけないな〜というのが悩みでしたが、TemplateStringTypesでそれが解決できたので作って見ました。order関数がそれに当たります。

optionsの中にあるadapterというのはsortの方法をカスタマイズするためのオプション引数です。

type NestColumn<T, U extends string> = T extends { [key: string]: unknown } ? `${U}.${Columns<T>}` : U;

type Columns<T extends { [key: string]: unknown }> = {
    [K in keyof T]: K extends string ? NestColumn<T[K], K> : never
}[keyof T]

type Split<T extends string, D extends string> = T extends `${infer Head}${D}${infer Tail}` ? [Head, ...Split<Tail, D>] : [T];

type O<T, C extends unknown[]> = T extends { [key: string]: unknown } ? Target<T, C> : never

type Target<T extends { [key:  string]: unknown }, C extends unknown[]> = {
    [K in keyof T]: C extends [infer Head, ...(infer Tail)]
                    ? K extends Head
                        ? Tail extends [unknown, ...(infer _Tail1)]
                            ? O<T[K], Tail> // Tailが残っているとき
                            : T[K] // 解析終了
                        : never // 対象のkeyじゃないとき
                    : never // そもそもCが空のとき
    }[keyof T]

type Options<T extends { [key: string]: unknown }, C extends unknown[], S extends string> = {
    adapter?: (t1: Target<T, C>, t2: Target<T, C>, sort: S) => number
}

function order<T extends { [key: string]: unknown }, C extends Columns<T>>(
    columns: C,
    sort: "asc" | "desc",
    options?: Options<T, Split<C, ".">, "asc" | "desc">) {
    const keys = columns.split(".") as Split<C, ".">

    return (obj1: T, obj2: T) => {
        let t1: any = obj1;
        let t2: any = obj2;
        keys.forEach(key => {
            t1 = t1[key]
            t2 = t2[key]
        })
        if (options?.adapter) return options.adapter(t1, t2, sort)

        if (sort === "asc") return t1 > t2 ? 1 : -1

        return t1 < t2 ? 1 : -1
    }
}

最後に

こういうのでもライブラリ化していいのかな〜オレオレ型定義ライブラリ作りてぇ〜〜〜〜〜

anyを使ってしまいました。懺悔します。

        let t1: any = obj1;
        let t2: any = obj2;

以上です。