TypeScript generic curried function with index type

Viewed 557

I have created a TypeScript curried function which first receives a property name as a string and then the object to get the value of that property from. I have used index type to make sure I get an error whenever I try to access a non-existing property:

export interface Dict {
  name: string;
  age: number;
}

const prop = <T extends Dict, K extends keyof T>(p: K) => (obj: T): T[K] => obj[p];

prop('name')({name: 'John', age: 45});  // John
prop('name2')({name: 'John', age: 45});  // error...

The last line gives the error:

error TS2345: Argument of type '"name2"' is not assignable to parameter of type '"name" | "age"'.

which is exactly what I want since the property name2 does not exist on the object given as the second argument.

However, when I try to create a safe version using a Maybe monad it gives a similar error:

const safeProp = <T extends Dict, K extends keyof T>(p: K) => (obj: T): Maybe<{}> => compose2(Maybe.of, prop(p))(obj);

The error is on the second argument of the compose2 function: prop(p):

Argument of type 'K' is not assignable to parameter of type '"name" | "age"'.

Which I don't understand since I have declared K extends keyof T which I assumed to be correct since it also works for the prop function.

For reference, the compose2 function:

const compose2 = <A, B, C>(f: (b: B) => C, g: (a: A) => B): ((a: A) => C) => a => f(g(a));

and the relevant part of Maybe monad:

class Maybe<A> {
  static of<A>(x: A): Maybe<A> {
    return new Maybe(x);
  }

  ...
}

How do I correctly type the safeProp function and why do I need to specify its return type as Maybe<{}> and not Maybe<T[K]>?

2 Answers

There's an issue here detailing how Typescript doesn't correctly infer types when passing generic functions into generic higher order functions (in your case, that's passing Maybe.of<A> into compose2<A,B,C>).

You can alleviate this by manually filling in the types for compose2 when using it.

const safeProp = <T extends Dict, K extends keyof T>(p: K) => (obj: T) => compose2<T, T[K], Maybe<T[K]>>(Maybe.of, prop(p))(obj);

safeProp now has the correct type const safeProp: <T extends Dict, K extends keyof T>(p: K) => (obj: T) => Maybe<T[K]>.

const safeProp = <T extends Dict, K extends keyof Dict>(p: K) => (obj: T): Maybe<{}> => compose2(Maybe.of, prop(p))(obj);

The above code doesn't give you an error and I'll try to explain why (and why your code does).

K extends keyof Dict is exactly name or age (which are only allowed parameters for function prop). And K extends keyof T can really be any key of type number | string | Symbol. Because if T extends Dict, it doesn't mean that it can't have any other keys like "lastname" etc. In other words:

type IsDict = {name: string, age: number, lastname: string} extends Dict ? true : false // IsDict is true

See? Your code would allow lastname key, and Typescript protects you from doing so.

That said, typescript currently does not have the means to restrict a type parameter to exact type (there are workarounds but unfortunately they don't suit your case).

Related