I'm creating a function like Lodash's at() . I have typing working if the user passes in tuples like this:
at(obj, ['key1'] as const, ['key2', 'key3'] as const)
I want the user to be able to call the function naturally, without tricks like " as const
". Can it be done? Here is the craziness I have so far:
type PropertyAtPath<T, Path extends readonly any[]> = Path extends []
? T
: Path extends readonly [infer First, ...infer Rest]
? First extends keyof T
? PropertyAtPath<T[First], Rest>
: undefined
: unknown;
type At<T, Paths extends ReadonlyArray<ReadonlyArray<any>>> = {
[I in keyof Paths]: Paths[I] extends readonly any[]
? PropertyAtPath<T, Paths[I]>
: never;
};
declare function at<T, Paths extends ReadonlyArray<ReadonlyArray<any>>>(
object: T,
...paths: Paths
): At<T, Paths>;
First of all, you need to validate second argument, to avoid passing invalid object paths.
Here , here , here and here, in my blog you can find an explanation of how this code works.
First three links are from stackoverflow. I have provided explanation in comments.
type Structure = {
foo: {
a: [1, 'hello'],
b: 2,
}
bar: {
c: 3,
d: 4,
}
}
declare var data: Structure;
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
/**
* Just like Array.prototype.reduce predicate/callback
* Receives accumulator and current element
* - if element extends one of accumulators keys -> return acc[elem]
* - otherwise return accumulator
*/
type Callback<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
/**
* If Keys extends a string with dot
*/
Keys extends `${infer Prop}.${infer Rest}`
/**
* - Call Reducer recursively with last property
*/
? Reducer<Rest, Callback<Accumulator, Prop>>
/**
* - Otherwise obtain whole property
*/
: Keys extends `${infer Last}`
? Callback<Accumulator, Last>
: never
{
type Test1 = Reducer<'foo.a', Structure> // 1
type Test2 = Reducer<'bar.d', Structure> // 4
}
/**
* Compute all possible property combinations
*/
type KeysUnion<T, Cache extends string = ''> =
/**
* If T extends string | number | symbol -> return Cache, this is the end
*/
T extends PropertyKey ? Cache : {
/**
* Otherwise, iterate through keys of T, because T is an object
*/
[P in keyof T]:
/**
* Check if property extends string
*/
P extends string
/**
* Check if it is the first call of this utility,
* because Cache is empty
*/
? Cache extends ''
/**
* If it is a first call,
* call recursively itself, go one level down - T[P] and initialize Cache - `${P}`
*/
? KeysUnion<T[P], `${P}`>
/**
* If it is not first call of KeysUnion and not the last
* Unionize Cache with recursive call, go one level dow and update Cache
*/
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
{
//"foo" | "bar" | "foo.a" | "foo.b" | "bar.c" | "bar.d"
type Test1 = KeysUnion<Structure>
}
type ExtractPath<T extends string> = Extract<T, string>
type Mapper<Obj, Paths extends ExtractPath<KeysUnion<Obj>>[]> = {
[Prop in keyof Paths]: Reducer<Paths[Prop] & string, Obj>
}
const at = <
Obj,
Key extends ExtractPath<KeysUnion<Obj>> & string,
Keys extends Key[]
>(obj: Obj, keys: [...Keys]): Mapper<Obj, Keys> =>
null as any
type Result = Mapper<Structure, ['foo.a.1', 'bar.c']>
const lookup = at(data, ['foo.a.0', 'bar.c']) // [1, 3]
Above code expects obj
to be fully infered, I mean obj
should be as const
.
If you want to handle arrays
and empty tuples you might want to use this implementation:
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 Concat<
Cache extends PropertyKey[],
Prop extends string | number | symbol
> =
Cache extends []
? [Prop]
: [...Cache, Prop]
/**
* Simple iteration through object properties
*/
type HandleObject<Obj, Cache extends PropertyKey[]> = {
[Prop in keyof Obj]:
| Cache
// concat previous Cacha and Prop
| Concat<Cache, Prop>
// with next Cache and Prop
| Path<Obj[Prop], Concat<Cache, Prop>>
}[keyof Obj]
type Path<Obj, Cache extends PropertyKey[] = []> =
(Obj extends PropertyKey
// return Cache
? Cache
// if Obj is Array (can be array, tuple, empty tuple)
: (Obj extends Array<any>
// 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, Concat<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], [...Cache, `${number}`]>
)
// if Obj is neither Array nor Tuple nor Primitive - treat is as object
: HandleObject<Obj, Cache>
)
)
type Reducer<Obj, Props extends Array<PropertyKey>> =
Props extends []
? Obj
: (Props extends [infer Fst, ...infer Tail]
? (Tail extends string[]
? (
Obj extends Array<any>
? (
Fst extends `${number}`
? Reducer<Obj[number], Tail>
: never)
: (Fst extends keyof Obj
? Reducer<Obj[Fst], Tail>
: never
)
)
: never
)
: never
)
type Validation<T> = IsNever<T> extends true ? [never] : []
const at = <
Obj,
Keys extends string[]
>(obj: Obj, keys: [...Keys], ...validation: Validation<Reducer<Obj, Keys>>): Reducer<Obj, [...Keys]> =>
null as any
type Structure = {
empty: [],
tuple: [1, 2, 3],
array: { age: Array<{ surname: string }> }[]
}
declare const data: Structure
/**
* Tests
*/
// [1, 2, 3]
const _ = at(data, ['tuple'])
// const __: {
// age: Array<{
// surname: string;
// }>;
// }
const __ = at(data, ['array', '0'])
// const ___: {
// surname: string;
// }[]
const ___ = at(data, ['array', '2', 'age'])
// const ____: {
// surname: string;
// }
const ____ = at(data, ['array', '0', 'age', '2'])
/**
* Expected never
*/
{
const _ = at(data, ['tupl'],) // error
const __ = at(data, ['array', 'w']) // error
}
In case someone stumbles upon this in the future.
If you just want to have the function infer a tuple instead of an array without using as const
, there are multiple options.
The preferred option is to use a variadic tuple type :
function fn<T extends string[]>(tuple: [...T]) { return tuple }
const r = fn(["a", "b", "c"])
// ^? const r: ["a", "b", "c"]
There is also this alternative:
function fn<T extends string[] | [string]>(tuple: T) { return tuple }
This behaves identical to the first example in most cases. However I encountered some differences in the past where the second solution works but the first one does not .
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.