Skip to content

Instantly share code, notes, and snippets.

@dmorosinotto
Last active October 13, 2021 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmorosinotto/617cf6f599a4cb0fa8ebf994b52c5531 to your computer and use it in GitHub Desktop.
Save dmorosinotto/617cf6f599a4cb0fa8ebf994b52c5531 to your computer and use it in GitHub Desktop.
TS strict GET with properties path / Tokenize
/**
* EnhanceTokenizedArray to resolve problem with Index array in path string.
* type Test = EnhanceTokenizedArray<Tokenize<'foo.0.bar', '.'>>; // Returns ['foo', number, 'bar']
*/
type StringLookingLikeANumber = `${number}`;
type EnhanceTokenizedArray<S> =
S extends [infer Head, ...infer Rest] ?
Head extends StringLookingLikeANumber ? [number, ...EnhanceTokenizedArray<Rest>]
: [Head, ...EnhanceTokenizedArray<Rest>]
: [];
/**
* Tokenize splits a string literal S by a delimeter D.
*/
type Tokenize<S extends string, D extends string> =
string extends S ? string[] : /* S must be a literal */
S extends `${infer T}${D}${infer U}` ? [T, ...Tokenize<U, D>] : /* Recursive case */
[S] /* Base case */
;
/**
* Navigate takes a type T and an array K, and returns the type of T[K[0]][K[1]][K[2]]...
*/
type Navigate<T, K extends(string|number)[]> =
T extends object ? /* T must be indexable (object or array) */
(K extends [infer K0, ...infer K1] ? /* Split K into head and tail */
(K0 extends keyof T ? /* head(K) must index T */
(K1 extends(string|number)[] ? /* tail(K) must be an array */
(Navigate<T[K0], K1>) /* explore T[head(K)] by tail(K) */ :
undefined) /* tail(K) was not an array, give up */ :
undefined) /* head(K) does not index T, give up */ :
T) /* K is empty; just return T */ :
T /* T is a primitive, return it */
;
/**
* Get takes a type T and an some property names or indices K.
* If K is a dot-separated string, it is tokenized into an array before proceeding.
* Then, the type of the nested property at K is computed: T[K[0]][K[1]][K[2]]...
* This works with both objects, which are indexed by property name, and arrays, which are indexed
* numerically.
*/
type Get<T, K> =
K extends string ? Get<T, EnhanceTokenizedArray<Tokenize<K, '.'>>>: /* dot-separated path */
K extends Array<string|number>? Navigate<T, K>: /* array of path components */
never;
type Party = {
venue: {
address: string,
dates: Array<{
month: number,
day: Date,
}>
},
}
// This evaluates to `string`.
type whereIsTheParty = Get<Party, 'venue.address'>;
// This evaluates to `Date`.
type whatDayIsTheParty = Get<Party, ['venue', 'dates', 0, 'day']>;
// This evaluates to `number`.
type whatMonthIsTheParty = Get<Party, 'venue.dates.123.month'>;
function safeGet<T, P extends string | ReadonlyArray<string|number> >(obj: T, path: P): Get<T,P> {
const paths: ReadonlyArray<string|number> = (typeof path === 'string') ? path.split('.') : path;
return paths.reduce((tmp:any, key) => tmp[key], obj) as Get<T,P>
}
var party: Party = {
venue: {
address: 'London',
dates: [
{month: 10, day: new Date(2021,10,13)}
, {month: 3, day: new Date(1975,3,20)}
]
}
}
let a = safeGet(party, 'venue.address'); // OK infer string
let b = safeGet(party, 'venue.dates.0.day'); // OK infer Date
let c = safeGet(party, ['venue','address'] as const); // THIS DON'T WORK I CAN'T FIND A WAY TO TRASFORM Array<string|number> TO TUPLE!!!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment