ramda / ramda

:ram: Practical functional Javascript
https://ramdajs.com
MIT License
23.82k stars 1.43k forks source link

Why type inference works well when using ramda with ts #3289

Open Inhye-Cheong opened 2 years ago

Inhye-Cheong commented 2 years ago

Hello. I am a front-end developer who enjoys using ramda(+ @types/ramda) + typescript. When using ramda, the type inference of typescript is often much better than using js built-in functions. ex) Object.keys etc.

So, suddenly, a question arose. Can you explain why type inference works so well with ramda? FYI, I'm not a user who knows even the deep stuff of fp-ts, monad, functor, etc.

CrossEye commented 2 years ago

Mostly we hear complaints about that, and have to point them to the DefinitelyTyped folks who maintain the TS typings.

So by the same token, all credit must go to them.

There is an effort underway to move them in-house, but it's not moving quickly.

Inhye-Cheong commented 2 years ago

@CrossEye

First of all, thanks for the reply.

I'm not using ramda heavily, so I didn't have any "complaints" about type inference as you described.

My question was, How does ramda do better type inference than js built-in functions?

For example,

import * as R from 'ramda';

const MOCK_DATA = {
  'first-key': 'this is first value',
  'second-key': 'this is second value',
}

// using ramda : The type inference of the `key` is correct.
const findKeyUsingRamda = (label: string) => R.keys(MOCK_DATA).find((key) => MOCK_DATA[key] === label);
// using js built-in functions : The type of `key` cannot be inferred.
const findKey = (label: string) => Object.keys(MOCK_DATA).find((key) => MOCK_DATA[key] === label);
                                                                        ~~~~~~~~~~~~~~ Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ 'first-key': string; 'second-key': string; }'.
  No index signature with a parameter of type 'string' was found on type '{ 'first-key': string; 'second-key': string; }'.ts(7053)
CrossEye commented 2 years ago

I know very little about TS typings, nor how these were implemented in DefinitelyTyped for Ramda. But if I were to hazard a guess, then I would expect that the type assigned to R.keys is something like "a function from an Object to an array of Strings" and the one assigned to Object .keys is more like "a function from an Object to a mixed array of Strings and Symbols."

I'm sure this can be looked up, and while I know I could find the Ramda version, I don't know where to look for the Object.prototype one.

Of course that more generic one should also apply to Ramda's keys as well, but I'm guessing the TS implementers took some signals from Ramda's Hindley-Milner-inspired types, and treat some of the signatures as more aspirational than pedantic.

lisumio commented 2 years ago

The main difference is that Ramda leverages a combination of generics and keyof operator while native Object.prototype.keys always returns array of strings.

@CrossEye

"a function from an Object to an array of Strings"

Looking at the implementation it goes even one step further - "a function from an Object to an array of Object keys" - which means that it returns a union containing each key:

export function keys<T extends object>(x: T): Array<keyof T>;

While Object.prototype.keys:

keys(o: object): string[];

Because Object.prototype.keys returns array of any strings TS can't determine what type is MOCK_DATA[key] going to be - while when called using Ramda it can guarantee that key will be a key of MOCK_DATA and therefore assume its type.