Last active
June 2, 2021 13:42
-
-
Save captain-yossarian/d7cbc7490e9479ed9f2f1f44919390ac to your computer and use it in GitHub Desktop.
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
{ | |
const obj = { | |
user: { | |
name: 'John' | |
} | |
} | |
const result1 = deepPickFinal(obj, 'user') // { name: string; } | |
const result2 = deepPickFinal(obj, 'user', 'name') // string | |
} | |
/** | |
* First of all, we should validate [rest] arguments, because they should be in right order. | |
* | |
* Example: we cant use [name] and then [user] | |
* | |
*/ | |
// Let's proceed with next object | |
type Foo = { | |
user: { | |
description: { | |
name: string; | |
surname: string; | |
} | |
} | |
} | |
/** | |
* In order to do some validation, we should | |
* generate a union of properties in right order with | |
* all possible combinations | |
* | |
*/ | |
type ExpectedUnion = | |
| ['user'] | |
| ['user', 'description'] | |
| ['user', 'description', 'name'] | |
| ['user', 'description', 'surname'] | |
/** | |
* Let's try to do this | |
* | |
*/ | |
type FirstAttempt<Obj> = { | |
[Prop in keyof Obj]: [Prop] | |
} | |
type Result1 = FirstAttempt<Foo> | |
// { | |
// user: ["user"]; | |
// } | |
/** | |
* Above code does not make any sense, for now. | |
* | |
* We need to iterate through every nested property, hence, we need to make it recursively. | |
* | |
* Let's try again. | |
* | |
* But now, we need call recursion only if property is not primitive. | |
*/ | |
// boolean is omited by purpose | |
type Primitives = string | number | symbol; | |
type SecondAttempt<Obj> = { | |
[Prop in keyof Obj]: Obj[Prop] extends Primitives ? [Prop] : SecondAttempt<Obj[Prop]> | |
} | |
/** | |
* You may winder why I'm using ['user']['description'] | |
* | |
* Because of this above condition, I will end up in deepest key with primitive value | |
*/ | |
type Result2 = SecondAttempt<Foo>['user']['description'] | |
// type Result2 = { | |
// name: ["name"]; | |
// surname: ["surname"]; | |
// } | |
/** | |
* Not good, not terrible | |
* | |
* At least we have two arrays with deepest Primitives | |
* | |
* It is looks better now, but we did not receive full path to [name] and [surname] | |
* | |
* We only have an array of deepest primitive property. | |
* | |
* Seems we need to provide some Cache | |
*/ | |
type ThirdAttempt<Obj, Cache extends Array<Primitives> = []> = { // added second generic parameter - Cache | |
[Prop in keyof Obj]: Obj[Prop] extends Primitives | |
? [...Cache, Prop] | |
: ThirdAttempt<Obj[Prop], [...Cache, Prop]> | |
} | |
type Result3 = ThirdAttempt<Foo>['user']['description'] | |
// type Result3 = { | |
// name: ["user", "description", "name"]; | |
// surname: ["user", "description", "surname"]; | |
// } | |
/** | |
* We have generated full pathes for deepest primitive values | |
* | |
* Still, this is not what we want. How about all possible values (unions) ? | |
* | |
* We can try to pass Cache as a union of previous and next type <------------------- important! | |
*/ | |
type FourthAttempt<Obj, Cache extends Array<Primitives> = []> = { | |
[Prop in keyof Obj]: Obj[Prop] extends Primitives | |
? [...Cache, Prop] | |
: FourthAttempt<Obj[Prop], Cache | [...Cache, Prop]> // <------ added Cache | [...Cache, P], in other words: Cache is unionized | |
} | |
type Result4 = FourthAttempt<Foo>['user']['description'] | |
// type Result4 = { | |
// name: ["name"] | ["user", "name"] | ["description", "name"] | ["user", "description", "name"]; | |
// surname: ["surname"] | ["user", "surname"] | ["description", "surname"] | [...]; | |
// } | |
/** | |
* Any questions so far? | |
* | |
* I know this is not readable at all | |
*/ | |
/** | |
* Seems we are closer now. | |
* Pls keep in mind, we still need a union of arrays instead of some weird object. | |
* | |
* Let's move our condition one level up | |
*/ | |
type FifthAttempt<Obj, Cache extends Array<Primitives> = []> = | |
Obj extends Primitives ? Cache : { // <------- condition moved out of mapped type scope | |
[Prop in keyof Obj]: FifthAttempt<Obj[Prop], Cache | [...Cache, Prop]> | |
} | |
/** | |
* We still have an object instead of array | |
*/ | |
type Result5 = FifthAttempt<Foo>['user']['description'] | |
// { | |
// name: [] | ["user"] | ["description"] | ["user", "description"] | ["name"] | ["user", "name"] | ["description", "name"] | ["user", "description", "name"]; | |
// surname: [] | ["user"] | ... 5 more ... | [...]; | |
// } | |
// for readability | |
type Values<T> = T[keyof T] | |
type ValuesExample = Values<{ age: number, name: string }> // string | number, like Object.values | |
type SixthAttempt<Obj, Cache extends Array<Primitives> = []> = | |
Obj extends Primitives ? Cache : Values<{ // <---- wrapped in Values | |
[Prop in keyof Obj]: SixthAttempt<Obj[Prop], Cache | [...Cache, Prop]> | |
}> | |
type Result6 = SixthAttempt<Foo> | |
// type Result6 = | |
// |[] | |
// | ["user"] | |
// | ["description"] | |
// | ["user", "description"] | |
// | ["name"] | |
// | ["user", "name"] <------------- WRONG!!!! | |
// | ["description", "name"] | |
// | ["user", "description", "name"] | |
// | ["surname"] | |
// | ["user", "surname"] | |
/** | |
* There is a logical error. Let's fix it. | |
* | |
* This was the hardest part for me | |
*/ | |
let error: Result6 = ['user', 'name'] // should not be allowed | |
type FinalAttempt<Obj, Cache extends Array<Primitives> = []> = | |
Obj extends Primitives ? Cache : { | |
[Prop in keyof Obj]: [...Cache, Prop] | FinalAttempt<Obj[Prop], [...Cache, Prop]> | |
}[keyof Obj] | |
type Result = FinalAttempt<Foo> | |
// type Result = | |
// | ["user"] | |
// | ["user", "description"] | |
// | ["user", "description", "name"] | |
// | ["user", "description", "surname"] | |
type Assert<T> = | |
FinalAttempt<Foo> extends T | |
? T extends FinalAttempt<Foo> | |
? true | |
: false | |
: false | |
type Test = Assert<ExpectedUnion> // true | |
let error2: Result = ['user', 'name'] // expected error | |
/** | |
* Tip: | |
* | |
* You can replace Values with explicit keyof to make it more readable on hover | |
*/ | |
declare function deepPick2<Obj,>(obj: Obj, ...keys: FinalAttempt<Obj>): void | |
declare var foo: Foo; | |
deepPick2(foo, 'user'); // ok | |
deepPick2(foo, 'user', 'description') // ok | |
deepPick2(foo, 'description') // expected error | |
/** | |
* We did 70% of our work so far | |
* | |
* Are you bored ? | |
* | |
* How about implement the function? | |
*/ | |
// type Foo = { | |
// user: { | |
// description: { | |
// name: string; | |
// surname: string; | |
// } | |
// } | |
// } | |
function deepPick3<Obj>(obj: Obj, ...keys: FinalAttempt<Obj>) { | |
return keys.reduce((acc, elem) => acc[elem], obj) // <--- Oooops, TS is unsure whether keys is array or not | |
} | |
/** | |
* Let's make a union of all values | |
* | |
* I hope this code looks familiar to you, | |
* Here, I want to get union of all | |
*/ | |
type ValuesUnion<Obj, Cache = Obj> = | |
Obj extends Primitives ? Obj : { | |
[Prop in keyof Obj]: | |
| Cache | Obj[Prop] | |
| ValuesUnion<Obj[Prop], Cache | Obj[Prop]> | |
}[keyof Obj] | |
type Example2 = ValuesUnion<Foo> | |
// string | Foo | { | |
// description: { | |
// name: string; | |
// surname: string; | |
// }; | |
// } | { | |
// name: string; | |
// surname: string; | |
// } | |
// typeguard | |
const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop) | |
: obj is Obj & Record<Prop, any> => | |
Object.prototype.hasOwnProperty.call(obj, prop); | |
const deepPick4 = < | |
Obj, | |
Keys extends FinalAttempt<Obj> & Array<string> // <-- we need to asure TS that keys is an array of strings | |
> | |
(obj: ValuesUnion<Obj>, ...keys: Keys) => | |
keys.reduce<ValuesUnion<Obj>>((acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc, obj) | |
const x = deepPick4(foo, 'user','description') // type is safe but not useful | |
/** | |
* Let's implement our return type | |
*/ | |
type Elem = string; | |
type ReduceAccumulator = Record<string, any> | |
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc | |
type Predicate<Accumulator extends ReduceAccumulator, El extends Elem> = | |
El extends keyof Accumulator // hasProperty(acc, elem) | |
? Accumulator[El] // acc[elem] | |
: Accumulator // acc | |
type Reducer< | |
Keys extends ReadonlyArray<any>, | |
Accumulator extends ReduceAccumulator = {} | |
> = | |
/** | |
* If Keys is empty array, no need to call recursion, | |
* just return Accumulator | |
*/ | |
Keys extends [] | |
? Accumulator | |
/** | |
* If keys is one element array, | |
* | |
*/ | |
: Keys extends [infer Head] | |
? Head extends Elem | |
/** | |
* take this element and call predicate | |
*/ | |
? Predicate<Accumulator, Head> | |
: never | |
/** | |
* If Keys is an Array of more than one element | |
*/ | |
: Keys extends readonly [infer Head, ...infer Tail] | |
? Tail extends ReadonlyArray<Elem> | |
? Head extends Elem | |
/** | |
* Call recursion with Keys Tail | |
* and call predicate with first element | |
*/ | |
? Reducer<Tail, Predicate<Accumulator, Head>> | |
: never | |
: never | |
: never; | |
function deepPickFinal<Obj, Keys extends FinalAttempt<Obj> & ReadonlyArray<string>> | |
(obj: ValuesUnion<Obj>, ...keys: Keys): Reducer<Keys, Obj> | |
function deepPickFinal<Obj, Keys extends FinalAttempt<Obj> & Array<string>> | |
(obj: ValuesUnion<Obj>, ...keys: Keys) { | |
return keys | |
.reduce( | |
(acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc, | |
obj | |
) | |
} | |
const result = deepPickFinal(foo, 'user') // ok | |
const result2 = deepPickFinal(foo, 'user', 'description') // ok | |
const result3 = deepPickFinal(foo, 'user', 'description', 'name') // ok | |
const result4 = deepPickFinal(foo, 'user', 'description', 'surname') // ok | |
/** | |
* Expected errors | |
*/ | |
const result5 = deepPickFinal(foo, 'surname') | |
const result6 = deepPickFinal(foo, 'description') | |
const result7 = deepPickFinal(foo) | |
/** | |
* Alternative, a bit shorter, way to validate arguments | |
* | |
*/ | |
type Util<Obj, Props extends ReadonlyArray<Primitives>> = | |
Props extends [] | |
? Obj | |
: Props extends [infer First] | |
? First extends keyof Obj | |
? Obj[First] | |
: never | |
: Props extends [infer Fst, ...infer Tail] | |
? Fst extends keyof Obj | |
? Tail extends string[] | |
? Util<Obj[Fst], Tail> | |
: never | |
: never | |
: never | |
type IsNeverType<T> = [T] extends [never] ? true : false; | |
type IsAllowed<T> = IsNeverType<T> extends true ? false : true; | |
type Validator<T extends boolean | string> = T extends true ? [] : [never] | |
function pick< | |
Obj, | |
Prop extends string, | |
Props extends ReadonlyArray<Prop>, | |
Result extends Util<Obj, Props>> | |
( | |
obj: Obj, | |
props: [...Props], | |
..._: Validator<IsAllowed<Result>> | |
): Result; | |
function pick< | |
Obj, | |
Prop extends string, | |
Props extends ReadonlyArray<Prop>, | |
Result extends Util<Obj, Props>>( | |
obj: Obj, | |
props: [...Props], | |
..._: Validator<IsAllowed<Result>>) { | |
return props.reduce( | |
(acc, prop) => hasProperty(acc, prop) ? acc[prop] : acc, | |
obj | |
) | |
} | |
const result8 = pick(foo, ['user']) // ok | |
const result9 = pick(foo, ['user', 'description', 'name']) // ok | |
/** | |
* Errors | |
*/ | |
const result10 = pick(foo, ['description']) // error | |
const result11 = pick(foo, ['name']) // ok | |
/** | |
* SUMMARY | |
* | |
* We know how to: | |
* - implement map/reduce for literal type | |
* - use cache with recursive types | |
* - use never for validation purpose | |
* | |
* Link to both examples: https://gist.github.com/captain-yossarian/d7cbc7490e9479ed9f2f1f44919390ac | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment