前提・実現したいこと
Electron + Vue + TypeScript にて、データベースから取得したデータを加工して表示するようなアプリケーションを作っています(今回はElectronとVueはあまり関係ない……と思います)。
この際_.mapValues
を多用するのですが、これをうまく型付けするのが難しく、悩んでいる次第です。
やりたいことを抽象化したコードは、次のようになります。
typescript
1import _ from 'lodash' 2 3const obj = { 4 a1: {b1: 1, b2: 'b'}, 5 a2: {b1:2, b3: 3} 6} 7 8const func = <T>(arg: T) => ({x1: {...arg, b4: 'c'}, x2: 1}) 9 10const result = _.mapValues(obj, func)
このときのtypeof result
は
typescript
1{ 2 a1: { 3 x1: {b1: number; b2: string; b4: string}; 4 x2: number 5 }; 6 a2: { 7 x1: {b1: number; b3: number; b4: string}; 8 x2: number 9 } 10}
であるはずです。これを推論させるのが目標となります。
試したこと
#####① _.mapValuesの型を定義するだけでいいなら最善のはず
_.mapValues
が@typesの型定義のままではうまくいかないということで、まずは次のようにしてみました。
typescript
1type MapValues<O, F> = { 2 (obj: O, func: F): { 3 [K in keyof O]: F extends {(n1: O[K]): infer R} ? R : never 4 } 5} 6 7const result = (_.mapValues as MapValues<typeof obj, typeof func>)(obj, func)
しかし、このときのtypeof result
は
typescript
1{ 2 a1: { 3 x1: {b4: string}; 4 x2: number 5 }; 6 a2: { 7 x1: {b4: string}; 8 x2: number 9 } 10}
となり、result.a1.x1.b1
などが存在しないことになってしまいます。
② 世の中にはいろいろライブラリがあるから……
この手の型の扱いは、関数型系統のライブラリなら強いはず、と思い、ramda.js、sanctuary.jsを試してみました。
しかし、ramda.jsのmapObjIndexed
、sanctuary.js、のmap
ともに、typeof result
は
typescript
1{ 2 [key: string]: { 3 x1: {b4: string;}; 4 x2: number; 5 }; 6}
となってしまいました。
③ 丁寧に書けば一応できる
前提のコードにおいて、typeof func
は下記になります。
typescript
1<T>(arg: T) => {x1: T & {b4: string;}; x2: number;}
そこで、
typescript
1type FuncType<T> = (arg: T) => {x1: T & {b4: string}; x2: number} 2type MapValues<O> = (obj: O, func: any) => {[K in keyof O]: ReturnType<FuncType<O[K]>>} 3 4const result= (_.mapValues as MapValues<typeof obj>)(obj, func)
とすることで、result
の型を正しく推論できるようになりました。
###では何が問題なのか
_.mapValues
を利用するたびに、FuncType
を定義する必要があります。<T>(arg: T) => (Tのナニカ)
という型から、FuncType<T> = (arg: T) => (Tのナニカ)
という型を生成できればいいのですが……。_.mapValues
を利用するたびに、MapValues
(そのときの_.mapValues
の型)を定義する必要があります。MapValues
の型引数としてFuncType
が渡せればいいのですが、型引数がついていないFuncType
は型ではないので、うまくいきません。
###望んでいる解決策
- (最上)mapValues的なものの型推論が希望通りに働くライブラリを見つける。
- (上)
FuncType
を毎回定義してくても、typeof func
からFuncType<T>
を生成できる、……ような感じのことをする。 - (中)
FuncType<T>
は毎回書くことにするが、type MapValues
は毎回定義しなくても、FuncType
を型引数にして生成できる、……ような感じのことをする。
mapValuesについてググってみると、同様の悩みを持っている人は多いようで、やはり難しそうではありますが、ご回答をよろしくお願いいたします。
###補足
_.mapValues
のcallbackは(value, key, object)の3引数を持ちますが、上記はとりあえずvalueだけ用いて試行錯誤した結果です。最終的にはすべて使えるのが目標です。
###続・試したこと(ミス修正しました)
完全な解決ではありませんが、実用上そこそこいい感じになったものを追記します。
typescript
1type ObjValue<O> = { 2 [K in keyof O]: { 3 [L in keyof { 4 [K in keyof O]: {[M in keyof O[keyof O]]: O[K][M]} 5 }[keyof O]]: O[K][L] 6 } & 7 { 8 [K in keyof O]: { 9 [L in {[M in keyof O]: keyof O[M]}[keyof O]]?: L extends keyof O[K] 10 ? O[K][L] 11 : never 12 } 13 }[keyof O] 14}[keyof O] 15 16const func = <T extends ObjValue<typeof obj>>(arg: T) => ({ 17 x1: {...arg, b4: 'c'}, 18 x2: 1 19}) 20 21type MapValues<O, F> = { 22 (obj: O, func: F): { 23 [K in keyof O]: F extends {(n1: O[K]): infer R} ? R : never 24 } 25} 26 27const result = (_.mapValues as MapValues<typeof obj, typeof func>)(obj, func)
こうすると、ObjValue<typeof obj>
が{b1: number; b2?: string; b3?: number}
となり、typeof result
は下記のようになります。
typescript
1{ 2 a1: {x1: {b1: number; b2?: string; b3?: number; b4: string}; x2: number}; 3 a2: {x1: {b1: number; b2?: string; b3?: number; b4: string}; x2: number}; 4}
本来の目標から見ると、型の情報はけっこう抜け落ちてしまっているものの、インテリセンスは実用上役に立つレベルで効きますので、最悪、本質的な解決ができないならこんな感じで行こうと思います。
とはいえ、やっぱり本質的な解決が望ましいので、まだまだ回答をお待ちしています。
あなたの回答
tips
プレビュー