6

Typescript error

(method) R.Static.prop(p: string): (obj: Record) => T (+3 overloads)

Returns a function that when supplied an object returns the indicated property of that object, if it exists.

Argument of type '(obj: Record) => T' is not assignable to parameter of type '(s: {}) => {}'. Types of parameters 'obj' and 's' are incompatible. Type '{}' is not assignable to type 'Record'. Index signature is missing in type '{}'.ts(2345)

enter image description here

The code:

https://ramdajs.com/docs/#lens

// Changes value of key in object without mutation
export const updateKey = (key: string) => R.lens(R.prop(key), R.assoc(key));

How this function is used

interface IMyObj {
  position: number;
  price: number;
  value: number;
}

const myObj: IMyObj = {
  position: 1,
  price: 100,
  value: 100
}

const updateKey = (key: string) => R.lens(R.prop(key), R.assoc(key));

const newObj = R.set(updateKey('value'), 200, myObj);

console.log('newObj', newObj); // {"position":1,"price":100,"value":200}

In my actual app this is what the signature of my IAsset objects look like:

export interface IAsset {
  [key: string]: string | number | undefined | boolean;
  availableSupply?: string;
  currency: string;
  exchange: string;
  exchange_base?: string;
  marketCap: number;
  name: string;
  percentage?: number;
  price: number;
  position?: number;
  value?: number;
  inWatchlist?: boolean;
}
// Changes value of key in object without mutation
export const updateKey = (key: IAsset[string]) => {
  if (key) {
    return R.lens(R.prop(key), R.assoc(key));  
  }
}

However it still produces this Typescript warning:

Argument of type '(obj: Record) => T' is not assignable to parameter of type '(s: {}) => {}'. Types of parameters 'obj' and 's' are incompatible. Type '{}' is not assignable to type 'Record'. Index signature is missing in type '{}'.ts(2345)

Also side note, Typescript is killing the fun of beautiful small 1-line Ramda functional functions.

Leon Gaban
  • 36,509
  • 115
  • 332
  • 529
  • from what place came type `'(s: {}) => {}`? – Przemyslaw Jan Beigert Mar 12 '19 at 16:07
  • @PrzemyslawPietrzak I believe because Typescript knows I'm changing an Object's Record or key, and R.prop works on Objects, and the updateKey function takes in an Object as well as a new value to change. `Argument of type '(obj: Record) => T' is not assignable to parameter of type '(s: {}) => {}'.` – Leon Gaban Mar 12 '19 at 16:24
  • 2
    As to "*Typescript is killing the fun of beautiful small 1-line Ramda functional functions*", I've heard a lot of this over the last two years. As a Ramda developer, I have not found it a core concern; we're writing a JS library and want to make it as pleasant as we can for JS developers. We'd love it if it worked well with Typescript, but we won't sacrifice our JS API to that. And as yet it seems that TS doesn't really support the full range of constructs we use. I'd love to see that change, but I'm not holding my breath. – Scott Sauyet Mar 12 '19 at 17:42

1 Answers1

2

For your original question:

I'm no expert in Ramda, so perhaps I'm missing something with how these types are used, but it looks like you can get rid of the type errors by making updateKey generic:

const updateKey = <T, K extends string>(key: K) => R.lens(R.prop<K, T>(key), R.assoc(key));

Note that the type inference here won't be great. If you hover over updateKey("value"), the inferred type will be <{}, "value">(key: "value") => Lens, so you may need explicitly specify type arguments in certain scenarios.


For your updated question:

There's definitely something I'm missing. Your interface for IAsset doesn't support numeric keys—only strings. So there should be no need to worry anything other than strings, however, let's assume for the sake of argument that you also want to handle numeric or symbolic keys.

If you look at @types/ramda, the single-parameter overloads of prop and assoc only accept strings:

/**
 * Returns a function that when supplied an object returns the indicated property of that object, if it exists.
 */
prop<T>(__: Placeholder, obj: T): <P extends keyof T>(p: P) => T[P];
prop<P extends keyof T, T>(p: P, obj: T): T[P];
prop<P extends string>(p: P): <T>(obj: Record<P, T>) => T;
prop<P extends string, T>(p: P): (obj: Record<P, T>) => T;

/**
 * Makes a shallow clone of an object, setting or overriding the specified property with the given value.
 */
assoc<T, U>(__: Placeholder, val: T, obj: U): <K extends string>(prop: K) => Record<K, T> & U;
assoc<U, K extends string>(prop: K, __: Placeholder, obj: U): <T>(val: T) => Record<K, T> & U;
assoc<T, U, K extends string>(prop: K, val: T, obj: U): Record<K, T> & U;
assoc<T, K extends string>(prop: K, val: T): <U>(obj: U) => Record<K, T> & U;
assoc<K extends string>(prop: K): <T, U>(val: T, obj: U) => Record<K, T> & U;

I'm not sure, but I believe this is an oversight on the @types/ramda project. You could enhance the type definition through declaration merging in this way:

interface Static {
    prop<P extends keyof T, T>(p: P): (obj: Record<P, T>) => T;
    assoc<K extends keyof T, T>(prop: K): <T, U>(val: T, obj: U) => Record<K, T> & U;
}

And then type your updateKey method like this, without the need for separate blocks to handle different key types:

const updateKey = <K extends keyof IAsset>(key: K) => R.lens(R.prop<K, IAsset>(key), R.assoc<K, IAsset>(key));
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
  • 1
    On second thought, this is probably **not** an oversight after all. In [this example](https://ramdajs.com/repl/?v=0.26.1#?const%20updateKey%20%3D%20%28k%29%20%3D%3E%20R.lens%28R.prop%28k%29%2C%20R.assoc%28k%29%29%0AR.set%28updateKey%280%29%2C%20%27foo%27%2C%20%5B%27bar%27%5D%29%3B), ramda seems to accept for numeric keys, but arrays are converted to objects and their keys are converted to strings. This leads me to conclude that these methods aren't actually intended for use with arrays or numeric strings and you should probably go with the first option I suggested. – p.s.w.g Mar 12 '19 at 17:57
  • Thank you! Oh and yeah in my IAsset, the key is always string, I messed up in my example. In my interface, actually `[key: string]: string | number | undefined | boolean;` Means Key is string, but that key can belong to a `string`, `number` `undefined` or `boolean. – Leon Gaban Mar 12 '19 at 18:31