Last active
October 13, 2021 14:01
-
-
Save dmorosinotto/617cf6f599a4cb0fa8ebf994b52c5531 to your computer and use it in GitHub Desktop.
TS strict GET with properties path / Tokenize
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
/** | |
* 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'>; |
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
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