2019-12-29
|~5 min read
|870 words
Recently, I needed the ability to track which items within a collection had been selected. As I thought about the problem, it felt like a great candidate for a custom hook. The specifics of the type of item didn’t matter, I just wanted to know which ones had been selected by the client (via a check box, drag and drop, etc.).
Writing the hook in Typescript, and because I didn’t know the specific type, I used a generic type for the basis of all of the typings within the hook.
Eventually, it looked something like:
function useSelected<T>(items?: T[], key?: keyof T) {
const [allItems, setAllItems] = useState(keyBy(items, key));
const [keyName, setKeyName] = useState(key)
const resetAllItems = (items: T[], key: keyof T) => {
setAllItems(keyBy(items, key));
setKeyName(key);
};
return {
allItems,
resetAllItems,
// ...
}
}
When it came time to actually use it, I invoked the hook in the following way1:
const { resetAllItems } = useSelected()
This approach, however, results in errors:
Caption: Argument of type '"MediaKey"' is not assignable to parameter of type 'never'.ts(2345)
My first question was, “Why is the the type never
even though I assigned a type when I define the function?”
It turns out it’s because Typescript is trying to infer based on the invocation of the hook2:
As a result, Typescript infers the types based on the items
prop that’s passed in (which is undefined). As a result, key, which is of keyof T
is now never
.
With the problem diagnosed, I now needed to figure out hwo to fix it!
I’ve found three different solutions to this problem of varying complexity.
Definitely the easiest way to get around this problem is to stop it before it starts. The hook takes two optional parameters, which if supplied, provide Typescript sufficient information to complete the inference.
What that means is … replace:
const { resetAllItems } = useSelected()
with:
const list = [
{ key: "abc", val: 123 },
{ key: "def", val: 456 },
]
const keyLabel = "key"
const { resetAllItems } = useSelected(list, keyLabel)
Voilà - errors begone! Typescript now knows what T
is by looking at the shape of an individual item in list
and the key
since it’s supplied as keyLabel
.
Alternatively, instead of passing in the argument, let Typescript know the type of the generic it will receive.
Remember, the hook definition is all based on a generic type T
:
function useSelected<T>(items?: T[], key?: keyof T) {
// ...
}
So, when it’s called, even if no values are passed, it’s possible to declare <T>
like so:
interface IItem {
key: string,
val: number
}
const { resetAllItems } = useSelected<IItem> ();
Do this and the errors will be gone because Typescript can now infer what items
, and consequently key
, will be … and it’s not never
but an array of the objects defined by IITem
and one of its keys.
A third approach to the problem is to effectively have multiple generics and deferring the inference as long as possible.
For example, modify the function definition of resetAllItems
to be:
function resetAllItems<U=T>(items: U[], key: keyof U) => {
setAllItems(keyBy(items, key));
setKeyName(key);
};
This would change the hook to be:
function useSelected<T>(items?: T[], key?: keyof T) {
const [allItems, setAllItems] = useState(keyBy(items, key));
const [keyName, setKeyName] = useState(key)
function resetAllItems<U=T>(items: U[], key: keyof U) => {
setAllItems(keyBy(items, key));
setKeyName(key);
};
return {
allItems,
resetAllItems,
// ...
}
}
Of course, this fixes one problem only to create another. Now setAllItems
and setKeyName
have typing issues - but they too could be modified.
So on, and so forth. In my case, this approach required more refactoring than I felt was reasonable. Particularly because I already had reasonable solutions that communicated the intent to future developers who might come across this code later. None the less, knowing that it’s possible to reassign generics in this way will surely be useful at some point.
My journey with Typescript still feels like it’s in its infancy. Writing this hook was the first time that I reached for a generic type intentionally and could even conceive of why it would be useful. Of course, once I did so, I uncovered an entirely new class of issues requiring consideration and new patterns for solving them!
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!