Search code examples
typescriptgenericstype-conversiontype-safety

Generic mapping of types in typescript


lets imagine type T:

type T = {
  prop1: (s: S) => T1,
  prop2: (s: S) => T2,
  prop3: (s: S) => T3,
}

and now lets imagine type W

type W = (s: S) => {
    prop1: T1,
    prop2: T2,
    prop3: T3,
}

its easy to write a function that maps T to W by hand,

is it possible to write generic type sefe version of it in typescript?

function x(t: T): W {
  return funtion(s: S) {
    prop1: t.prop1(s),
    prop2: t.prop2(s)
    prop3: t.prop3(s)
  }
}

what kind of feature language is missing to facilitate this, something like higher order generic types?


Solution

  • You can indeed write a generic version of this in TypeScript:

    function x<S, V>(t: {[K in keyof V]: (s: S) => V[K]}): (s: S) => V {
      return function(s: S): V {
        const ret = {} as V;
        Object.keys(t).forEach((k: keyof V) => {
          ret[k] = t[k](s);
        })
        return ret;
      }
    } 
    
    const xSpecific: (t: T) => W = x; // okay
    

    Note that V is the return type of your W function. (So W is essentially the same as (s: S) => V.) And the input to x is a mapped type corresponding to T: it has the same keys as V, but its values are functions from S to the corresponding properties of V.

    You can get away with having the function input being a mapped type and the output being an unmapped one because TypeScript supports inference from mapped types. Otherwise you'd need something like the proposed "extended typeof" feature to derive W from T generically. (This might be the missing language feature to which you're alluding.)

    As for the implementation, I'm looping over the keys of t and applying each function t[k] to the input s.

    And xSpecific is the same as x narrowed to the particular T and W types you posted. This compiles because TypeScript recognizes that the generic x is compatible.


    Now for the caveats and fine print. Unfortunately, the compiler isn't able to reliably infer the type of S from x directly. If you just call the generic x() with a T input, you get this:

    declare const t: T;
    const w = x(t); // (s: {}) => { prop1: T1; prop2: T2; prop3: T3; }
    

    The w is not exactly a W... it accepts any input, not just an S. If you really need to narrow the type of input, you'll have to do it yourself by manually specifying the generic parameters:

    const w = x<S, {prop1: T1, prop2: T2, prop3: T3}>(t);
    

    which is ugly, or narrowing the resulting w manually by assertion or annotation:

    const w: W = x(t);
    

    Anyway, hope that helps. Good luck!