DEV Community

Irakλi Safareli
Irakλi Safareli

Posted on

You could have invented lenses!

Imagine you have a JavaScript code like this:

const someObj = { tags: [], packages: [] };
function hasValue(key, item) {
  return someObj[key].indexOf(item) !== -1;
}
Enter fullscreen mode Exit fullscreen mode

And you are porting this this into TypeScript. You are trying to follow best practices, like using strict mode. And don’t want to use any and unsafe casts. so you start adding types:

type SomeType = {
  tags: (string | number)[];
  packages: string[];
};
const someObj: SomeType = { tags: [], packages: [] };
Enter fullscreen mode Exit fullscreen mode

This was easy, but then you hit one issue that you can’t really write type for hasValue without using some sort of casting. This is your best attempt:

function hasValue<K extends keyof SomeType>(key: K, item: SomeType[K]) {
  return someObj[key].indexOf(item) !== -1;
  //                   ^^^^
  // Argument of type 'SomeType[K]' is not assignable to parameter of type 'string'.
  //   Type '(string | number)[] | string[]' is not assignable to type 'string'.
  //     Type '(string | number)[]' is not assignable to type 'string'
  //
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, looks like typescript can’t understand that type of item depends on the key and it's safe to do indexOf like this. You start tearing your hear if you still have any but don't quite know what to do. and then you get a great idea, instead of using property access using [...] why not pass some sort of getter function to the hasValue. You feel excited, and start to add more types, more code!

type Getter<Obj, Param> = (o: Obj) => Param;

function hasValue<Val>(getter: Getter<SomeType,Val[]>, item: Val) {
  return getter(someObj).indexOf(item) !== -1;
}
Enter fullscreen mode Exit fullscreen mode

And boom! it works. But you have another similar function which also needs to set value. Yeah you guessed it right, we need some setter function

type Setter<Obj, Param> = (o: Obj, val: Param): void;

function resetIfHasValue<Val>(
  getter: Getter<SomeType,Val[]>,
  setter: Setter<SomeType,Val[]>,
  item: Val
) {
  if(hasValue(getter,val)){
    setter(someObj,[])
  }
}
Enter fullscreen mode Exit fullscreen mode

Nice, but we would have to define getters and setters for each property and we would have to be passing them as 2 separate values to this function. Would be nicer if we created one type containing both:

type Modifier<Obj, Param>
  = Setter<Obj, Param>
  & Getter<Obj, Param>;

type Setter<Obj, Param> = {
  set(o: Obj, val: Param): void;
};

type Getter<Obj, Param> = {
  get(o: Obj): Param;
};

function resetIfHasValue<Val>(
  modifier: Modifier<SomeType,Val[]>,
  item: Val
) { ... }
Enter fullscreen mode Exit fullscreen mode

We are onto something here and it starts to look nice. but we still would have to define all the modifiers by hand, but why should we, we are developers let’s write code that writes code for us :D :

const keyModifier = <Obj>() => <Key extends keyof Obj>(
  key: Key
): Modifier<Obj, Obj[Key]> => ({
  set(o: Obj, val: Obj[Key]) {
    o[key] = val;
  },
  get(o: Obj): Obj[Key] {
    return o[key];
  },
});

const tags = keyModifier<SomeType>()("tags")
const packages = keyModifier<SomeType>()("packages")
Enter fullscreen mode Exit fullscreen mode

Now this will compile and work just fine:

resetIfHasValue(tags, 1)
resetIfHasValue(tags, "dd")
resetIfHasValue(packages, "asd")
Enter fullscreen mode Exit fullscreen mode

You notice that Getters are composable as they are just pure functions, but the fact that setters are mutating values is not that nice. Tho, at this point we have invented quasi mutable lenses and If you really want you might invent actual purely functional lenses as well, but that’s for another day. Meanwhile you can take a look at an actual typescript lesses library from @gcanti / monocle-ts.

Top comments (0)