Last active
August 6, 2023 22:34
-
-
Save zachhardesty7/65ff817661487a3bfb02a5c698825df9 to your computer and use it in GitHub Desktop.
TS: pathify interface TypeScript (generate all possible paths in string format)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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