Codereview.ts[0]
Published on May 20, 2022
I really like TS (when I know what I’m doing)
My pet peeve lately are tutorials that teach code which is not typesafe. I don’t mind tutorials written in JS, in fact I think it’s often a better option to avoid any cognitive overhead when learning a new topic.
But, I’ve seen so many tutorial where unsafe actions are leveraged.
I just needed to get that off of my chest.
Code Review
Recently I was shown this snippet of code
export const removeFromArr = <T, K extends keyof T>(
elementToRemove: T,
arrayToRemoveElementFrom: T[] | undefined,
identifyingField?: K // K is limited to the keys of type T
): T[] => {
// If there is nothing in the array, return empty array
if (!arrayToRemoveElementFrom) return [];
// If there's a field, we try to avoid checking by referrence and value
if (identifyingField) {
// If the element to remove does not have a
// value at the field to check we remove nothing
if (!elementToRemove[identifyingField]) return arrayToRemoveElementFrom;
// Else we remove by that field
return arrayToRemoveElementFrom.filter(
(element) =>
element[identifyingField] !== elementToRemove[identifyingField]
);
}
// Lastly remove by reference and value
return arrayToRemoveElementFrom.filter(
(element) => element !== elementToRemove
);
};
There is nothing wrong with this of course. But it’s possibly more verbose than it could be
Additionally, I feel as though this function is trying to solve two different problems. Removing an element from an array, and removing an object with a specifc key:value pair from an array. So firstly, I would prefer these to be two different functions.
This way, the identifying field no longer needs to be optional. As we are explicitly defining the array arrayToRemoveElementFrom
as an array, and not undefined, we can also remove the guard clause
export const removeFromArr = <T>(
arrayToRemoveElementFrom: T[],
elementToRemove: T,
): T[] => arrayToRemoveElementFrom.filter((element) => element !== elementToRemove)
export const removeFromArr = <T, K extends keyof T>(
arrayToRemoveElementFrom: T[],
elementToRemove: T,
identifyingField: K
): T[] => {
// If the element to remove does not have a
// value at the field to check we remove nothing
if (!elementToRemove[identifyingField]) return arrayToRemoveElementFrom;
// Else we remove by that field
return arrayToRemoveElementFrom.filter(
(element) =>
element[identifyingField] !== elementToRemove[identifyingField]
);
};
This is looking a lot cleaner, however I still think there is room for improvement. If elementToRemove[idendifyingField] === 0
this function will do nothing. This guard clause is pointless because identifyingField
has to exist as a key of elementToRemove
and as such, the array can be of any type. Additionally, interacting with this function can be made much simpler by specifying a key to check and a value to find in that key. Now instead of calling this function in the pattern of
removeFromArr(arrayToRemoveElementFrom, elementToRemove, identifyingField)
We can call it as
removeObjectFromArrByKey(array, key, value)
So my final snippet has ended up as
export const removeFromArr = <T>(
arrayToRemoveElementFrom: T[],
elementToRemove: T
): T[] =>
arrayToRemoveElementFrom.filter((element) => element !== elementToRemove)
export const removeObjectFromArrByKey = <T, K extends keyof T>(
arrayToRemoveElementFrom: T[], // an array of generic types
key: K, // The key of the element to remove
value: T[K] // A value from the one item in the array
): T[] => arrayToRemoveElementFrom.filter((e) => e[key] !== value)
const person1 = { name: "Mike", age: 32 }
const person2 = { name: "Steve", age: 25 }
const person3 = { name: "James", age: 35 }
const myArr = [person1, person2, person3]
removeObjectFromArrByKey(myArr, "age", 32) // valid
removeObjectFromArrByKey(myArr, "name", "mike") // valid
removeObjectFromArrByKey(myArr, "name", person1.name) // valid
removeObjectFromArrByKey(myArr, "title", "Mr") // error, the key is invalid
removeObjectFromArrByKey(myArr, "age", "thirty") // error, the value is of the wrong type
While the result seems a little contrived, this is a real-world example, that I found in the wild and there is no harm in having utility functions to make our lives easier.
I think this is a good example of how to use generics to both enforce better type safety, but also make code more readable and maintainable. Most importantly, how slippery a slope it can be to write overly complex functions when something simple will do the same job. I am a huge advocate for simplicity and I think this snippet is testament to benefit of trying to make code as easy to understand as possible.
No Unchecked Indexed Access
One other consideration is removing a key:value pair when the value is undefined. While I don’t think this is an issue, while running through this, I also learned about the, noUncheckedIndexedAccess
flag, which allows objects, or arrays to describe values as potentially undefined.
This does something pretty fun, but not directly related to our use case here
Consider this
type ObjOfArrays<T> = Record<string, any[]>
const studentSubjects: ObjOfArrays<string> = {
names: ["Jonh", "Henry", "Chris"],
subjects: ["Maths", "Science", "Art"],
}
studentSubjects.names.push('Reginald')
studentSubjects.names
does not exist, but our typing allows us to call an array method on it. To mitigate this risk, we can add an undefined union, which does something really cool, if you type out the last line again, TS will automatically add optional chaining in
type ObjOfArrays<T> = Record<string, any[] | undefined>
const studentSubjects: ObjOfArrays<string> = {
names: ["Jonh", "Henry", "Chris"],
subjects: ["Maths", "Science", "Art"],
}
studentSubjects.names?.push('Reginald')
But needing to add an undefined onto each case where you have a similar use case can be inconvenient.
// tsconfig.json
{
"compilerOptions": {
...
"noUncheckedIndexedAccess": false
},
}
Now the TS compiler will help us confirm or check if values are defined, without requiring an undefined union
type ObjOfArrays<T> = Record<string, any[]>
const studentSubjects: ObjOfArrays<string> = {
names: ["Jonh", "Henry", "Chris"],
subjects: ["Maths", "Science", "Art"],
}
studentSubjects.names?.push('Reginald')
// or
if (studentSubjects.names) studentSubjects.names.push('Reginald')