5

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 2

2
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).

Sign up to request clarification or add additional context in comments.

3 Comments

Good explanation, thanks a lot! I did however accept y2bd's answer since it is even more complete and allows to indicate the correct return type.
@DannyMoerkerke Ok, you are welcome! But didn't the problem be with prop function? I thought Maybe and stuff is irrelevant and the problem was prop(p) being error-highlighted.
Yes the main problem was with the prop function but like I said I was also wondering why the return type was Maybe<{}>. Specifying the types for the compose2 function explained it all for me.
1

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]>.

3 Comments

Great answer, thank you. I also added the return type for completeness: const safeProp3 = <T extends Dict, K extends keyof T>(p: K) => (obj: T): Maybe<T[K]> => compose2<T, T[K], Maybe<T[K]>>(Maybe.of, prop(p))(obj);
A PR has recently been merged for TypeScript that maybe enable better inference for this situation in the future: github.com/Microsoft/TypeScript/pull/30215
Great, thanks! I will have a good look tomorrow but I guess these are the propagated generic type arguments that will be in the coming TypeScript 3.4 release? devblogs.microsoft.com/typescript/announcing-typescript-3-4-rc

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.