Skip to content

Instantly share code, notes, and snippets.

@paavohuhtala
Last active March 29, 2018 18:05
Show Gist options
  • Save paavohuhtala/aa7cdc262e26fdd4dfc1e60198c18d9b to your computer and use it in GitHub Desktop.
Save paavohuhtala/aa7cdc262e26fdd4dfc1e60198c18d9b to your computer and use it in GitHub Desktop.
Crazy evil type system hacking to get partial.lenses working in TypeScript.
declare module "partial.lenses" {
// This is a fairly simple, heterogenous type-level linked list.
// We use this to represent lense types in an easy-to-use way.
type ConsNil = { "__NIL__": never };
type Cons<A, B> = { head: A, tail: B | ConsNil }
// Just a bit of syntax sugar.
type ConsLast<A> = Cons<A, ConsNil>
// TupleToCons converts a tuple type into a linked list.
// Due to the lack of variadic generics, the different arities have to implemented separately.
export type TupleToCons<T> =
T extends [infer A, infer B, infer C] ? Cons<A, Cons<B, ConsLast<C>>> :
T extends [infer A, infer B] ? Cons<A, ConsLast<B>> :
T extends [infer A] ? Cons<A, ConsNil> :
never;
// This is where it gets complicated.
// First of, this is an interface instead of a type alias, because TS doesn't support recursion
// for generic type aliases. The different is not that important, because we never construct this
// type at runtime.
//
// This results in a tree-like type, where every `.next` prop advances to the next step of the lens.
// The final `.next` is of the type the lens points to.
//
// V is the type of the object or array the lens is pointing to.
// C is the remaining part of the lens - either a Cons cell or a ConsNil.
export interface LensIter<V, C> {
next:
// If the lens is empty, return the value we reached.
C extends ConsNil ? V :
// Destructure the lens to get the head and the tail.
C extends Cons<infer Head, infer Tail> ?
// Arrays
Head extends number ? V extends Array<infer E> ?
LensIter<V[Head], Tail> :
// If head was a number but V is not an array, return undefined.
undefined :
// Objects
Head extends keyof V ?
LensIter<V[Head], Tail> :
// If head is not a valid key, return undefined.
undefined
: never
}
// We need to traverse the LensIter to get to the end of the .next chain.
// This is the type level counterpart to a chained property access like `lensIter.next.next.next.next`.
// Unfortunately for aforementioned reasons we need to have a fixed maximum depth.
type TraverseLensIter<T> = T extends { next: infer E } ? Traverse1<E> : T;
type Traverse1<T> = T extends { next: infer E } ? Traverse2<E> : T;
type Traverse2<T> = T extends { next: infer E } ? Traverse3<E> : T;
type Traverse3<T> = T extends { next: infer E } ? Traverse4<E> : T;
type Traverse4<T> = T extends { next: infer E } ? Traverse5<E> : T;
type Traverse5<T> = T extends { next: infer E } ? Traverse6<E> : T;
type Traverse6<T> = T extends { next: infer E } ? Traverse7<E> : T;
type Traverse7<T> = T extends { next: infer E } ? Traverse8<E> : T;
type Traverse8<T> = T extends { next: any } ? "DEPTH LIMIT" : T;
// The final signature is pretty simple. Convert the lens tuple into a linked list and start the iteration.
// After the iterator has been constructed, traverse it to get the return type.
function get<O, L>(lens: L, obj: O): TraverseLensIter<LensIter<O, TupleToCons<L>>>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment