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;
}
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: [] };
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'
//
}
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;
}
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,[])
}
}
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
) { ... }
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")
Now this will compile and work just fine:
resetIfHasValue(tags, 1)
resetIfHasValue(tags, "dd")
resetIfHasValue(packages, "asd")
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 down the rabbit hole 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 lenses library from @gcanti/monocle-ts.