Search code examples
typescripttypescript-genericsfp-ts

How does fp-ts get the `HKT<F, T>` type to unify to `F<T>`


I've read the explanation of HKT https://github.com/gcanti/fp-ts/blob/master/docs/guides/HKT.md and have started reading the code a bit...I understand the embedding and have been able to use it, but I'm not quite sure how it works viz the typescript compiler. The part that seems magical to me is that when you use the end method, the type will be F<T>, not HKT<F,T>...I'm curious how that bridge happens


Solution

  • Stripping away the fp-ts related code and HKT workarounds and just looking at TypeScript, consider this code:

    type Shape<T> = T extends Array<any> ? 'array' : 'value';
    
    interface IFoo<T> {
        value: Shape<T>;
    }
    
    type test1 = IFoo<number>['value'];
    //    ^? "value"
    type test2 = IFoo<number[]>['value'];
    //    ^? "array"
    
    

    Playground

    When TypeScript is working out the types of indexed fields in an interface, it attempts to simplify the value as much as possible. In this example, you can see that it evaluates the conditional Shape type and returns the literal type value.

    Returning to the HKT approach from fp-ts, we have something similar. There is an interface URItoKind which exists to point from a developer chosen string to a generic type. TypeScript doesn't let you pass around generic types like Shape but it will let you pass around string literals and it allows you to index into interfaces with those literals. It will also attempt to simplify the types as much as possible.

    For something like Functor<T>, the URI 'Functor' is chosen as a key, the type is stashed in URIToKind so that it can be looked up in the higher kinded functions, and when you have a concrete instance, the type checker is able to resolve down to Functor<T> again, pulling away the layers.

    Here is my own annotation of the identity example to illustrate what's happening.

    // Functor1 is going to pull values out of URIToKind
    import { Functor1 } from 'fp-ts/Functor'
    
    // Pick a name for the Identity type in the map.
    export const URI = 'Identity'
    
    // Make that chosen name available outside the module
    export type URI = typeof URI
    
    declare module 'fp-ts/HKT' {
      // Set up a mapping from the literal string we chose to the actual
      // * -> * Identity generic type
      interface URItoKind<A> {
        readonly Identity: Identity<A>
      }
    }
    
    // Make Identity available outside the module (and actually define behavior)
    export type Identity<A> = A
    
    // Functor instance
    // Functor1<URI> is doing a lot of work here. URI is the 'Identity' literal
    // we chose, and `Functor1` is looking into the `URIToKind` map, indexing
    // it with 'Identity' to get out the `Identity<A>` type which TS will then
    // see it cannot refine further because it doesn't know `A` yet. It ends
    // up with <A>(ma: Identity<A>, (a: A) => A) => A
    export const Functor: Functor1<URI> = {
      URI,
      map: (ma, f) => f(ma)
    }