この目標を達成するには、許可されているすべてのパスの順列を作成する必要があります。例:
type Structure = {
user: {
name: string,
surname: string
}
}
type BlackMagic<T>= T
// user.name | user.surname
type Result=BlackMagic<Structure>
配列と空のタプルを使用すると、問題がさらに興味深いものになります。
明示的な長さの配列であるタプルは、次の方法で管理する必要があります。
type Structure = {
user: {
arr: [1, 2],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.0" | "user.arr.1"
type Result = BlackMagic<Structure>
論理は単純明快です。しかし、number[]
をどのように処理できるか ?インデックス1
の保証はありません 存在します。
user.arr.${number}
を使用することにしました 。
type Structure = {
user: {
arr: number[],
}
}
type BlackMagic<T> = T
// "user.arr" | `user.arr.${number}`
type Result = BlackMagic<Structure>
まだ1つの問題があります。空のタプル。要素がゼロの配列-[]
。インデックス作成を許可する必要がありますか?知らない。 -1
を使用することにしました 。
type Structure = {
user: {
arr: [],
}
}
type BlackMagic<T> = T
// "user.arr" | "user.arr.-1"
type Result = BlackMagic<Structure>
ここで最も重要なことは、いくつかの慣習だと思います。文字列化された`"never"を使用することもできます。どのように扱うかはOP次第だと思います。
さまざまなケースを処理する必要があることがわかっているので、実装を開始できます。続行する前に、いくつかのヘルパーを定義する必要があります。
type Values<T> = T[keyof T]
{
// 1 | "John"
type _ = Values<{ age: 1, name: 'John' }>
}
type IsNever<T> = [T] extends [never] ? true : false;
{
type _ = IsNever<never> // true
type __ = IsNever<true> // false
}
type IsTuple<T> =
(T extends Array<any> ?
(T['length'] extends number
? (number extends T['length']
? false
: true)
: true)
: false)
{
type _ = IsTuple<[1, 2]> // true
type __ = IsTuple<number[]> // false
type ___ = IsTuple<{ length: 2 }> // false
}
type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
type _ = IsEmptyTuple<[]> // true
type __ = IsEmptyTuple<[1]> // false
type ___ = IsEmptyTuple<number[]> // false
}
ネーミングとテストは自明だと思います。少なくとも私は信じたい:D
これで、utilsのセットがすべて揃ったら、メインのutilを定義できます:
/**
* If Cache is empty return Prop without dot,
* to avoid ".user"
*/
type HandleDot<
Cache extends string,
Prop extends string | number
> =
Cache extends ''
? `${Prop}`
: `${Cache}.${Prop}`
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends string> = {
[Prop in keyof Obj]:
// concat previous Cacha and Prop
| HandleDot<Cache, Prop & string>
// with next Cache and Prop
| Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]
type Path<Obj, Cache extends string = ''> =
// if Obj is primitive
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<unknown>
// and is tuple
? (IsTuple<Obj> extends true
// and tuple is empty
? (IsEmptyTuple<Obj> extends true
// call recursively Path with `-1` as an allowed index
? Path<PropertyKey, HandleDot<Cache, -1>>
// if tuple is not empty we can handle it as regular object
: HandleObject<Obj, Cache>)
// if Obj is regular array call Path with union of all elements
: Path<Obj[number], HandleDot<Cache, number>>)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>)
)
// "user" | "user.arr" | `user.arr.${number}`
type Test = Extract<Path<Structure>, string>
小さな問題があります。 user
のような最高レベルの小道具を返さないでください 。少なくとも1つのドットが含まれるパスが必要です。
2つの方法があります:
- ドットなしですべての小道具を抽出します
- レベルにインデックスを付けるための追加の汎用パラメータを提供します。
2つのオプションは簡単に実装できます。
dot (.)
ですべての小道具を入手する :
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
上記のutilは読み取り可能で保守可能ですが、2番目のutilは少し難しいです。両方のPath
に追加の汎用パラメータを指定する必要があります およびHandleObject
。他の
type KeysUnion<T, Cache extends string = '', Level extends any[] = []> =
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`, [...Level, 1]>
: Level['length'] extends 1 // if it is a higher level - proceed
? KeysUnion<T[P], `${Cache}.${P}`, [...Level, 1]>
: Level['length'] extends 2 // stop on second level
? Cache | KeysUnion<T[P], `${Cache}`, [...Level, 1]>
: never
: never
}[keyof T]
正直なところ、誰もがこれを読むのは簡単ではないと思います。
もう1つ実装する必要があります。計算されたパスで値を取得する必要があります。
type Acc = Record<string, any>
type ReducerCallback<Accumulator extends Acc, El extends string> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Acc = {}
> =
// Key destructure
Keys extends `${infer Prop}.${infer Rest}`
// call Reducer with callback, just like in JS
? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
// this is the last part of path because no dot
: Keys extends `${infer Last}`
// call reducer with last part
? ReducerCallback<Accumulator, Last>
: never
{
type _ = Reducer<'user.arr', Structure> // []
type __ = Reducer<'user', Structure> // { arr: [] }
}
Reduce
の使用に関する詳細情報を見つけることができます <私のブログのrel="nofollow noreferrer noopener"href ="https://catchts.com/tuples#reduce">
。
コード全体:
type Structure = {
user: {
tuple: [42],
emptyTuple: [],
array: { age: number }[]
}
}
type Values<T> = T[keyof T]
{
// 1 | "John"
type _ = Values<{ age: 1, name: 'John' }>
}
type IsNever<T> = [T] extends [never] ? true : false;
{
type _ = IsNever<never> // true
type __ = IsNever<true> // false
}
type IsTuple<T> =
(T extends Array<any> ?
(T['length'] extends number
? (number extends T['length']
? false
: true)
: true)
: false)
{
type _ = IsTuple<[1, 2]> // true
type __ = IsTuple<number[]> // false
type ___ = IsTuple<{ length: 2 }> // false
}
type IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
type _ = IsEmptyTuple<[]> // true
type __ = IsEmptyTuple<[1]> // false
type ___ = IsEmptyTuple<number[]> // false
}
/**
* If Cache is empty return Prop without dot,
* to avoid ".user"
*/
type HandleDot<
Cache extends string,
Prop extends string | number
> =
Cache extends ''
? `${Prop}`
: `${Cache}.${Prop}`
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends string> = {
[Prop in keyof Obj]:
// concat previous Cacha and Prop
| HandleDot<Cache, Prop & string>
// with next Cache and Prop
| Path<Obj[Prop], HandleDot<Cache, Prop & string>>
}[keyof Obj]
type Path<Obj, Cache extends string = ''> =
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<unknown>
// and is tuple
? (IsTuple<Obj> extends true
// and tuple is empty
? (IsEmptyTuple<Obj> extends true
// call recursively Path with `-1` as an allowed index
? Path<PropertyKey, HandleDot<Cache, -1>>
// if tuple is not empty we can handle it as regular object
: HandleObject<Obj, Cache>)
// if Obj is regular array call Path with union of all elements
: Path<Obj[number], HandleDot<Cache, number>>)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>)
)
type WithDot<T extends string> = T extends `${string}.${string}` ? T : never
// "user" | "user.arr" | `user.arr.${number}`
type Test = WithDot<Extract<Path<Structure>, string>>
type Acc = Record<string, any>
type ReducerCallback<Accumulator extends Acc, El extends string> =
El extends keyof Accumulator ? Accumulator[El] : El extends '-1' ? never : Accumulator
type Reducer<
Keys extends string,
Accumulator extends Acc = {}
> =
// Key destructure
Keys extends `${infer Prop}.${infer Rest}`
// call Reducer with callback, just like in JS
? Reducer<Rest, ReducerCallback<Accumulator, Prop>>
// this is the last part of path because no dot
: Keys extends `${infer Last}`
// call reducer with last part
? ReducerCallback<Accumulator, Last>
: never
{
type _ = Reducer<'user.arr', Structure> // []
type __ = Reducer<'user', Structure> // { arr: [] }
}
type BlackMagic<T> = T & {
[Prop in WithDot<Extract<Path<T>, string>>]: Reducer<Prop, T>
}
type Result = BlackMagic<Structure>
これ 実装は検討する価値があります