Skip to content

Instantly share code, notes, and snippets.

@captain-yossarian
Last active June 2, 2021 13:42
Show Gist options
  • Save captain-yossarian/d7cbc7490e9479ed9f2f1f44919390ac to your computer and use it in GitHub Desktop.
Save captain-yossarian/d7cbc7490e9479ed9f2f1f44919390ac to your computer and use it in GitHub Desktop.
{
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