Last active
March 29, 2018 18:05
-
-
Save paavohuhtala/aa7cdc262e26fdd4dfc1e60198c18d9b to your computer and use it in GitHub Desktop.
Crazy evil type system hacking to get partial.lenses working in TypeScript.
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
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