Skip to content

Instantly share code, notes, and snippets.

@zachhardesty7
Last active August 6, 2023 22:34
Show Gist options
  • Save zachhardesty7/65ff817661487a3bfb02a5c698825df9 to your computer and use it in GitHub Desktop.
Save zachhardesty7/65ff817661487a3bfb02a5c698825df9 to your computer and use it in GitHub Desktop.
TS: pathify interface TypeScript (generate all possible paths in string format)
// copyright 2022 Zach Hardesty
// want to check this out in the TypeScript playground?
// visit the following link to automatically see the latest version!
// https://www.typescriptlang.org/play?jsx=0#gist/65ff817661487a3bfb02a5c698825df9
/** test type of obj w nesting */
type T000 = {
a1: {
b1: boolean
}
a2: {
b1: {
c1: boolean
}
b2: {
c1: boolean
c2: boolean
}
b3: boolean
}
a3: boolean
}
const X000 = {
a1: {
b1: true,
},
a2: {
b1: {
c1: false,
},
b2: {
c1: false,
c2: true,
},
b3: true,
},
a3: true,
}
/**
* extracts a union of string representations of all terminal paths that index into a
* static object type
*
* this is a recursive helper that breaks apart an object into its respective terminal
* paths, where a terminal path is a sequence of keys that, when used to repeatedly
* index into an object, resolves to a non-indexable type.
*
* NOTE: this is not designed to be used with types other than `object` and simple
* primitives. it's possible to achieve reasonable results when using the `TFilter` with
* arrays, but the usefulness of such resulting types seems questionable
*
* @example
* type Paths = Pathify<{ a: boolean; b: { c: string }; d: string }, string>
* // => "b.c" | "d"
* type PrefixedPaths = `prefix.${Pathify<
* { a: boolean; b: { c: string }; d: string },
* string
* >}`
* // => "prefix.b.c" | "prefix.d"
*
* @template TObj - full object to pathify
* @template TFilter - optionally include only paths resolving to this type
* @template TKeys - internal helper
*/
type Pathify<TObj, TFilter = unknown, TKeys = keyof TObj> = TKeys extends keyof TObj &
string
? TObj[TKeys] extends object // we can go deeper
? `${TKeys}.${Pathify<TObj[TKeys], TFilter>}` // recurse
: TObj[TKeys] extends TFilter // optional check to only allow given types
? TKeys // base case
: never
: never
type T101 = Expect<Pathify<{ a: string; b: [boolean] }>, "a" | "b.0" | "b.length">
type T102 = Expect<Pathify<{ c: { d: boolean } }>, "c.d">
type T103 = Expect<Pathify<{ c: { d: boolean }; e: { f: boolean } }>, "c.d" | "e.f">
type T104 = Expect<
Pathify<{
c: {
d: { e: boolean; f: boolean }
g: { h: boolean; i: boolean }
}
}>,
"c.d.e" | "c.d.f" | "c.g.h" | "c.g.i"
>
type T105 = Expect<Pathify<{ c: { d: { e: boolean; f: boolean } } }>, "c.d.e" | "c.d.f">
type T106 = Expect<
Pathify<{ a: boolean; b: boolean; c: { d: string } }>,
"a" | "b" | "c.d"
>
/** max property nesting depth due to TS recursion limit */
type T107 = Expect<
Pathify<{
c: {
d: {
e: {
f: {
g: {
h: {
i: {
j: {
k: {
l: {
m: {
n: {
o: {
p: { q: boolean }
}
}
}
}
}
}
}
}
}
}
}
}
}
}>,
"c.d.e.f.g.h.i.j.k.l.m.n.o.p.q"
>
type T108 = Expect<
Pathify<{ a: boolean; b: boolean; c: { d: string; e: boolean } }, string>,
"c.d"
>
type T109 = Expect<
Pathify<T000>,
"a3" | "a1.b1" | "a2.b1.c1" | "a2.b2.c1" | "a2.b2.c2" | "a2.b3"
>
// #region - incomplete reverse approach to check individual path against object
/** check for a property (aka key) matching `P` at any depth in `T` */
type DeepKeySearch<T, P> = T extends Record<infer K, unknown> // T is an object?
? P extends keyof T // T has a key matching P?
? boolean // found key matching P
: DeepKeySearch<Exclude<T[K], boolean>, P> // no match, recurse deeper into object but exclude primitives
: never // no more nested objects, only primitives: failed search, invalid type
// test cases
type T201 = Expect<DeepKeySearch<T000, "c1">, boolean>
type T202 = Expect<DeepKeySearch<T000, "nope">, never>
type T203 = Expect<DeepKeySearch<T000, "b1">, boolean>
type T204 = Expect<DeepKeySearch<T000, "a1">, boolean>
type T205 = Expect<DeepKeySearch<T000, "a2">, boolean>
type T206 = Expect<DeepKeySearch<T000, "a3">, boolean>
// quick POC property access string splitter
type Split<T> = T extends `${infer A}.${infer B}` ? [A, B] : never
type split = Expect<Split<"a.b">, ["a", "b"]>
// #endregion
// #region - utils
/**
* passes thru passing test input, errors with inputs that are not equal
*
* can be used in the middle of an expression and composed to check each step
*
* @todo check for perf issues with recursion
*/
type Expect<
Input extends Intersect,
Expected extends Intersect,
Intersect = Expected & Input,
> = Input
// #endregion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment