mono/packages/core/src/pick.ts
2025-02-12 20:47:47 +01:00

79 lines
2.1 KiB
TypeScript

import { Split } from 'type-fest'
/**
* Recursively pick the sub-object from T according to the array of keys in Keys.
*
* If Keys = ['address', 'coords', 'lat'],
* we want to produce { address: { coords: { lat: number } } } at the type level.
*/
type DeepPickArray<T, Keys extends readonly string[]> =
Keys extends []
? T
: Keys extends [infer Head, ...infer Tail]
? Head extends keyof T
? {
[P in Head]: Tail extends string[]
? DeepPickArray<T[Head], Tail>
: never
}
: never
: never;
/**
* Given a single path string (e.g. "address/coords/lat") and a separator (default = "."),
* parse that path into an array via `Split`, then recursively pick the resulting type.
*/
type DeepPickOne<
T,
Path extends string,
Sep extends string = "."
> = DeepPickArray<T, Split<Path, Sep>>;
/**
* If DeepPickOne<T, Path, Sep> yields `never` (meaning invalid path),
* we allow a fallback type D (default = undefined).
*/
type DeepPickValue<
T,
Path extends string,
Sep extends string = "/",
D = undefined
> = DeepPickOne<T, Path, Sep> extends infer R
? [R] extends [never]
? D
: R
: D;
export function pick <
T,
Path extends string,
Sep extends string = "/",
D = undefined
>(
obj: T,
path: Path,
options?: {
separator?: Sep;
defaultValue?: D;
}
): DeepPickValue<T, Path, Sep, D> {
// Determine separator (default to "/")
const separator = options?.separator ?? "/";
// Split the path into keys
const keys = path.split(separator);
// Traverse the object
let current: any = obj;
for (const key of keys) {
if (current == null || !(key in current)) {
// If we can't go further, return defaultValue (if provided)
return (options?.defaultValue as any)
?? (undefined as DeepPickValue<T, Path, Sep, D>);
}
current = current[key];
}
// If we successfully traversed the entire path, return the value
return current as DeepPickValue<T, Path, Sep, D>;
}