質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.80%

TypeScriptで_.mapValuesの型付け

受付中

回答 0

投稿 編集

  • 評価
  • クリップ 2
  • VIEW 788

7Kreuz

score 112

前提・実現したいこと

Electron + Vue + TypeScript にて、データベースから取得したデータを加工して表示するようなアプリケーションを作っています(今回はElectronとVueはあまり関係ない……と思います)。

この際_.mapValuesを多用するのですが、これをうまく型付けするのが難しく、悩んでいる次第です。

やりたいことを抽象化したコードは、次のようになります。

import _ from 'lodash'

const obj = {
  a1: {b1: 1, b2: 'b'},
  a2: {b1:2, b3: 3}
}

const func = <T>(arg: T) => ({x1: {...arg, b4: 'c'}, x2: 1})

const result = _.mapValues(obj, func)

このときのtypeof result

{
  a1: {
    x1: {b1: number; b2: string; b4: string};
    x2: number
  };
  a2: {
    x1: {b1: number; b3: number; b4: string};
    x2: number
  }
}

であるはずです。これを推論させるのが目標となります。

試したこと

① _.mapValuesの型を定義するだけでいいなら最善のはず

_.mapValuesが@typesの型定義のままではうまくいかないということで、まずは次のようにしてみました。

type MapValues<O, F> = {
  (obj: O, func: F): {
    [K in keyof O]: F extends {(n1: O[K]): infer R} ? R : never
  }
}

const result = (_.mapValues as MapValues<typeof obj, typeof func>)(obj, func)

しかし、このときのtypeof result

{
  a1: {
    x1: {b4: string};
    x2: number
  };
  a2: {
    x1: {b4: string};
    x2: number
  }
}

となり、result.a1.x1.b1などが存在しないことになってしまいます。

② 世の中にはいろいろライブラリがあるから……

この手の型の扱いは、関数型系統のライブラリなら強いはず、と思い、ramda.js、sanctuary.jsを試してみました。
しかし、ramda.jsのmapObjIndexed、sanctuary.js、のmapともに、typeof result

{
  [key: string]: {
    x1: {b4: string;};
    x2: number;
  };
}


となってしまいました。

③ 丁寧に書けば一応できる

前提のコードにおいて、typeof funcは下記になります。

<T>(arg: T) => {x1: T & {b4: string;}; x2: number;}


そこで、

type FuncType<T> = (arg: T) => {x1: T & {b4: string}; x2: number}
type MapValues<O> = (obj: O, func: any) => {[K in keyof O]: ReturnType<FuncType<O[K]>>}

const result= (_.mapValues as MapValues<typeof obj>)(obj, func)


とすることで、resultの型を正しく推論できるようになりました。

では何が問題なのか

  1. _.mapValuesを利用するたびに、FuncTypeを定義する必要があります。<T>(arg: T) => (Tのナニカ)という型から、FuncType<T> = (arg: T) => (Tのナニカ)という型を生成できればいいのですが……。
  2. _.mapValuesを利用するたびに、MapValues(そのときの_.mapValuesの型)を定義する必要があります。MapValuesの型引数としてFuncTypeが渡せればいいのですが、型引数がついていないFuncTypeは型ではないので、うまくいきません。

望んでいる解決策

  1. (最上)mapValues的なものの型推論が希望通りに働くライブラリを見つける。
  2. (上)FuncTypeを毎回定義してくても、typeof funcからFuncType<T>を生成できる、……ような感じのことをする。
  3. (中)FuncType<T>は毎回書くことにするが、type MapValuesは毎回定義しなくても、FuncTypeを型引数にして生成できる、……ような感じのことをする。

mapValuesについてググってみると、同様の悩みを持っている人は多いようで、やはり難しそうではありますが、ご回答をよろしくお願いいたします。

補足

_.mapValuesのcallbackは(value, key, object)の3引数を持ちますが、上記はとりあえずvalueだけ用いて試行錯誤した結果です。最終的にはすべて使えるのが目標です。

続・試したこと(ミス修正しました)

完全な解決ではありませんが、実用上そこそこいい感じになったものを追記します。

type ObjValue<O> = {
  [K in keyof O]: {
    [L in keyof {
      [K in keyof O]: {[M in keyof O[keyof O]]: O[K][M]}
    }[keyof O]]: O[K][L]
  } &
    {
      [K in keyof O]: {
        [L in {[M in keyof O]: keyof O[M]}[keyof O]]?: L extends keyof O[K]
          ? O[K][L]
          : never
      }
    }[keyof O]
}[keyof O]

const func = <T extends ObjValue<typeof obj>>(arg: T) => ({
  x1: {...arg, b4: 'c'},
  x2: 1
})

type MapValues<O, F> = {
  (obj: O, func: F): {
    [K in keyof O]: F extends {(n1: O[K]): infer R} ? R : never
  }
}

const result = (_.mapValues as MapValues<typeof obj, typeof func>)(obj, func)

こうすると、ObjValue<typeof obj>{b1: number; b2?: string; b3?: number}となり、typeof resultは下記のようになります。

{
  a1: {x1: {b1: number; b2?: string; b3?: number; b4: string}; x2: number};
  a2: {x1: {b1: number; b2?: string; b3?: number; b4: string}; x2: number};
}


本来の目標から見ると、型の情報はけっこう抜け落ちてしまっているものの、インテリセンスは実用上役に立つレベルで効きますので、最悪、本質的な解決ができないならこんな感じで行こうと思います。

とはいえ、やっぱり本質的な解決が望ましいので、まだまだ回答をお待ちしています。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

まだ回答がついていません

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.80%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る