jamesmcnamara / shades

A lodash-inspired lens-like library for Javascript
MIT License
413 stars 14 forks source link

Typings for virtual lenses #17

Closed spiffytech closed 5 years ago

spiffytech commented 5 years ago

I'm trying to create a virtual lens that casts a string | number to a number. It works in the Node REPL, but gives TypeScript errors.

const amountAsNumber = {
  get: (amount) => parseFloat(amount.toString()),
  mod: (fn) => (amount) => fn(amount),
}

const getAmount = shades.get('amount', amountAsNumber);

The shades.get line has the following error: Argument of type '"amount"' is not assignable to parameter of type 'Lens<{}, any>'.

jamesmcnamara commented 5 years ago

Very good question. There probably needs to be better documentation here.

The heart of the issue is that your get function is of type string | number => number but your mod function is of type

(f: (v: string | number) => string | number) => string | number => string | number)

Which isn't consistent with a mod function's signature. A mod function for a lens of type A to B should have signature:

(f: (v: B) => B) => A => A

Or in English: "Take a type-preserving function over the focus type of the lens (number in this case), and return a type-preserving function for the source type (string | number here)"

So it should be:

(f: (v: number) => number) => string | number => string | number)

amount is a string | number and fn is a number => number. You need to convert amount from a string | number into a number before you call fn (and normally you would then need to convert the result of fn back to a string | number afterwards, but number is already a subtype of string | number)

So a more correct lens would look like:

const toNum = (amt: string | number): number => parseFloat(amt.toString())

const amountAsNumber = {
  get: toNum,
  mod: fn => amount => fn(toNum(amount)),
}

Strictly Typing Virtual Lenses

Getting the types to fully typecheck has some gotcha's as of right now (I'm fixing them before v2)

First import the Lens and mark the type:

import {Lens} from 'shades/types/utils';

...

const amountAsNumber: Lens<string | number, number> = {
  ...
}

However, this will still yell at you saying that your lens is missing a traversal property. This is a boolean value that states whether you want it to apply as a traversal or not. For this, we don't, so we add:

const amountAsNumber: Lens<string | number, number> = {
  ...
  traversal: false
}

And now everything should typecheck!

Putting it all together:

import {Lens} from 'shades/types/utils';

const toNum = (amt: string | number) => parseFloat(amt.toString())

const idAsNumber: Lens<string | number, number> = {
  get: toNum,
  mod: fn => amount => fn(toNum(amount)),
  traversal: false
};

There is more information on creating and typing Virtual Lenses here

spiffytech commented 5 years ago

Excellent, works perfectly. Thanks for the thorough explanation!

jamesmcnamara commented 5 years ago

P.S. This has been fixed in the new beta

If you do:

npm install shades@beta

The new code would just be:

import {Lens} from 'shades';

const toNum = (amt: string | number) => parseFloat(amt.toString())

const idAsNumber: Lens<string | number, number> = {
  get: toNum,
  mod: fn => amount => fn(toNum(amount))
};