指定したキーだけ変換するutility
ObjectKeyPaths
code:typescript
// CurrentPathのないパターン
type ObjectKeyPaths<T extends object> = T extends any[]
// 配列の場合
? T extends (infer U)[]
? U extends object
? U extends any[]
? '[]' | []${ObjectKeyPaths<U>}
: '[]' | [].${ObjectKeyPaths<U>}
: '[]'
: never
// タプルの場合
? U extends object
? U extends any[]
? '[]' | []${ObjectKeyPaths<U>} | [${Rest['length']}]${ObjectKeyPaths<U>} | ObjectKeyPaths<Rest>
: '[]' | [].${ObjectKeyPaths<U>} | [${Rest['length']}].${ObjectKeyPaths<U>} | ObjectKeyPaths<Rest>
: '[]' | [${Rest['length']}] | ObjectKeyPaths<Rest>
: never
: {
? K | ${K}${ObjectKeyPaths<T[K]>}
? K | ${K}.${ObjectKeyPaths<T[K]>}
: K
: never
code:typescript
// CurrentPathあるパターン
type JoinObjectKey<CurrentPath extends string, AppendKey extends string> =
CurrentPath extends ''
? AppendKey
: ${CurrentPath}.${AppendKey}
type ObjectKeyPaths<T extends object, CurrentPath extends string = ''> = T extends any[]
// 配列の場合
? T extends (infer U)[]
?
| ${CurrentPath}[]
| (U extends object
? ObjectKeyPaths<U, ${CurrentPath}[]>
: never
)
: never
// タプルの場合
?
// []はいらないかも
| ${CurrentPath}[] | ${CurrentPath}[${Rest['length']}]
| (U extends object
? ObjectKeyPaths<U, ${CurrentPath}[] | ${CurrentPath}[${Rest['length']}]>
: never
)
| ObjectKeyPaths<Rest, CurrentPath>
: never
: {
?
| JoinObjectKey<CurrentPath, K>
? ObjectKeyPaths<TK, JoinObjectKey<CurrentPath, K>> : never
)
: never
GetPaths
.は単純split
[]と[number]はこの文字列も残してsplit
code:typescript
type Split<Str extends string, Separator extends string, Preserve = false> = Str extends ${infer Key}${Separator}${infer Rest}
?
[
...(Key extends '' ? [] : Key), ...(Preserve extends true ? [Str extends ${Key}${infer Matched}${Rest} ? Matched : Separator] : []),
...GetPaths<Rest>
]
: Str extends ''
? []
type SplitArrayPaths<KeyPaths extends string[], Separator extends string> = KeyPaths extends [infer KeyPath extends string, ...infer Rest extends string[]]
: []
type GetPaths<KeyPath extends string> = SplitArrayPaths<
SplitArrayPaths<Split<KeyPath, '.'>, '[]'>,
[${number}]
type paths = GetPaths<'foo.arr[].buz1'> // ['foo', 'arr', '[]', 'buz', '1'] GetTypeByPath
GetPathsのkeyリストを元に、目的のパスに降りていく
code:typescript
type GetTypeByKeys<T, Paths extends string[]> = Paths extends [infer Key extends string, ...infer Rest extends string[]]
? T extends any[]
? Key extends '[]'
? GetTypeByKeys<Tnumber, Rest> : Key extends [${infer Index extends number}]
? GetTypeByKeys<TIndex, Rest> : never
: Key extends keyof T
? GetTypeByKeys<TKey, Rest> : never
: T
type GetTypeByPath<T extends object, KeyPath extends ObjectKeyPaths<T>> = GetTypeByKeys<
T,
GetPaths<KeyPath>
// type TText = GetTypeByPath<typeof nestObj, 'obj.text'>
ChangeTypeByKey
GetPathsのkeyリストを元に目的のパスに降りていき、最終地点でNewTypeに差し替える
code:typescript
type ChangeTupleTypeByIndex<T extends any[], Index extends number, NewType> = T extends ...infer Rest, infer U : T
type ChangeTypeByPaths<T, Paths extends string[], NewType> = Paths extends [infer Key extends string, ...infer Rest extends string[]]
? T extends any[]
? Key extends '[]'
? Array<ChangeTypeByPaths<Tnumber, Rest, NewType>> : Key extends [${infer Index extends number}]
? ChangeTupleTypeByIndex<
T,
Index,
ChangeTypeByPaths<TIndex, Rest, NewType> : T
: Key extends keyof T
? {
? ChangeTypeByPaths<TK, Rest, NewType> }
: T
: NewType
type ChangeTypeByKey<T extends object, KeyPath extends ObjectKeyPaths<T>, NewType> = ChangeTypeByPaths<
T,
GetPaths<KeyPath>,
NewType
// type TText = ChangeTypeByKey<typeof nestObj, 'obj.objDate', Date>
transformByKey
上記のtype定義に倣って実装する。
code:typescript
const preserveSplit = (str: string, separator: string | RegExp): string[] => {
const matched = str.match(separator)
if (matched == null) {
}
const matchedText = matched0 const matchedIndex = matched.index || 0
const prevText = str.slice(0, matched.index)
return [
matchedText,
...preserveSplit(str.slice(matchedIndex + matchedText.length), separator)
]
}
const getPaths = (keyPath: string): string[] => {
return keyPath
.split('.')
.map((path) => preserveSplit(path, /(\\)|(\\d\)/)) .flat()
}
const transformByPaths = (obj: any, paths: string[], transformer: (value: any) => any): any => {
if (path == null) {
return transformer(obj)
}
if (Array.isArray(obj)) {
const arr = obj
if (path === '[]') {
return arr.map((item) => transformByPaths(item, restPaths, transformer))
}
const match = path.match(/\(\d)\/) if (match) {
const index = parseInt(match1) return [
...arr.slice(0, index),
transformByPaths(arrindex, restPaths, transformer), ...arr.slice(index + 1)
]
}
return arr
}
const keys = Object.keys(obj)
if (keys.includes(path)) {
return {
...obj,
path: transformByPaths(objpath, restPaths, transformer) }
}
return obj
}
const transformByKey = <
T extends object,
Key extends ObjectKeyPaths<T>,
Transformer extends (value: GetTypeByPath<T, Key>) => any
(obj: T, keyPath: Key, transformer: Transformer): ChangeTypeByKey<T, Key, ReturnType<Transformer>> => {
return transformByPaths(obj, getPaths(keyPath), transformer)
}
上記パターンだとオブジェクトから一括変換することができない
再帰しようにもRecordの時点で順不同で結果が不安定になってしまうため
code:typescript
// イメージしたもの
Object.keys(transformerSet).reduce((obj, key) => {
return transformByKey(obj, key, transformerSetkey) }, obj)
ChangeTypeByKeyValueSet
最初からオブジェクトのものを渡して、その時に一致するkeyをvalueに差し替える。
探索方法はルートから全体を見ていき、途中までのパスがあっていないオブジェクトはそのまま返し、先頭部分がマッチしているものだけ深ぼっていく。
code:typescript
type FilterStartsWith<S extends PropertyKey, Start extends string> = S extends ${Start}${infer Rest} ? S : never
type ChangeTypeByKeyValueSetImpl<T, KeyValueSet extends Record<string, any>, CurrentPath extends string = ''> =
FilterStartsWith<keyof KeyValueSet, CurrentPath> extends never
? T
: CurrentPath extends keyof KeyValueSet
: T extends any[]
// 配列の場合
? Array<ChangeTypeByKeyValueSetImpl<Tnumber, KeyValueSet, ${CurrentPath}[]>> // タプルの場合
? [
...ChangeTypeByKeyValueSetImpl<Rest, KeyValueSet, CurrentPath>,
ChangeTypeByKeyValueSetImpl<U, KeyValueSet, ${CurrentPath}[${Rest['length']}]>
]
: T
: keyof T extends never
? T
: {
? ChangeTypeByKeyValueSetImpl<TK, KeyValueSet, JoinObjectKey<CurrentPath, K>> }
type ChangeTypeByKeyValueSet<T extends object, KeyValueSet extends Partial<{ K in ObjectKeyPaths<T>: any }>> = // ObjectKeyPathsにないKeyを指定していたらエラーと気づけるようにneverを返す
Exclude<keyof KeyValueSet, ObjectKeyPaths<T>> extends never
? ChangeTypeByKeyValueSetImpl<T, KeyValueSet>
: never
type newType = ChangeTypeByKeyValueSet<typeof complecatedObj, { "objArr.arr[].objArrObjDate": Date }>
transform
上記のtype定義に倣った実装。
code:typescript
const joinObjectKey = (currentPath: string, key: string) => {
return currentPath === '' ? key : ${currentPath}.${key}
}
const transformImpl = (obj: any, transformerSet: Partial<Record<string, (value: any) => any>>, currentPath = ''): any => {
// 現在のパスがtransformerSetに含まれていない場合は変換処理を行わずそのまま返す
const filteredMatchedPaths = Object.keys(transformerSet).filter((path) => path.startsWith(currentPath))
if (filteredMatchedPaths.length <= 0) {
return obj
}
// 完全一致のtransformerがある場合は実行する
if (filteredMatchedPaths.includes(currentPath)) {
return transformer ? transformer(obj) : obj
}
if (Array.isArray(obj)) {
const arr = obj
// 次のpathに配列指定が含まれているか
const isNextArrPath = filteredMatchedPaths.some((path) => path.startsWith(${currentPath}[]))
return arr.map((item, index) => {
// 配列指定が含まれている場合は優先的に配列指定、そうでない場合はタプル指定にする
const nextPath = isNextArrPath ? ${currentPath}[] : ${currentPath}[${index}]
return transformImpl(item, transformerSet, nextPath)
})
}
if (typeof obj === 'object') {
const keys = Object.keys(obj)
return Object.assign({}, ...keys.map((key) => ({
key: transformImpl(objkey, transformerSet, joinObjectKey(currentPath, key)) })))
}
return obj
}
type TransformedMap<TransformerSet extends Partial<Record<string, (value: any) => any>>> = {
}
const transform = <
T extends object,
(
obj: T,
transformerSet: TransformerSet
): ChangeTypeByKeyValueSet<T, TransformedMap<TransformerSet>> => {
return transformImpl(obj, transformerSet)
}
GetTypeByKey
pathを分割しないで下っていくバージョン
code:typescript
type GetTypeByKeyImpl<T, Key extends string, CurrentPath extends string = ''> = Key extends ${CurrentPath}${infer Rest}
? Rest extends ''
// CurrentPathがKeyと一致したらTを返す
? T
: T extends any[]
// 配列の場合
? GetTypeByKeyImpl<Tnumber, Key, ${CurrentPath}[]> // タプルの場合
?
| GetTypeByKeyImpl<U, Key, ${CurrentPath}[${ArrRest['length']}]>
| GetTypeByKeyImpl<ArrRest, Key, CurrentPath>
: never
: keyof T extends never
? never
// オブジェクトの場合
: {
? GetTypeByKeyImpl<TK, Key, JoinObjectKey<CurrentPath, K>> : never
: never
type GetTypeByKey<T extends object, Key extends ObjectKeyPaths<T>> = GetTypeByKeyImpl<T, Key>
/icons/hr.icon
汎用的なtransformから日付専用のtransformに切り出すのは型都合上難しそう
どうしてもやるなら始めから日付に特化したtypeとかにしておかないと推論で死ぬ