Typing a classic JS one-liner

One line to rule them all

typescript, functional-programming

Often we have a list of things and would like to know how many there are of each thing in the list. This is one of those handy one-liners that will give you an object mapping an item to the number of occurrences for a given list of things.

JS
const getFreqMap = (list) =>
list.reduce((acc, cur) =>
({ ...acc, [cur]: (acc[cur] ?? 0) + 1 }), {})
getFreqMap(['a', 'a', 'c', 'b', 'd', 'd', 'd'])
// {a: 2, c: 1, b: 1, d: 3}
getFreqMap([0, 1, 2, 2, 6, 2, 1, 0, 6])
// {0: 2, 1: 2, 2: 3, 6: 2}

Okay so maybe its not just one line.
But that's because I formatted it nicely!
At least its not as bad as this:

JS
const getFreqMap = (list) => {
const res = {}
for (let i = 0; i < list.length; i++) {
if (res[list[i]] !== undefined){
res[list[i]]++
}
else{
res[list[i]] = 1
}
}
return res
}

Wow, what a waste of lines...
Anyway, lets rewrite our one-liner in typescript because its better.


TSJS

So lets just change the file type to .ts.
...And immediately we get a bollocking like so:

1
Parameter 'list' implicitly has an 'any' type.
2
Parameter 'acc' implicitly has an 'any' type.
3
Parameter 'cur' implicitly has an 'any' type.
TS
contains errors
const getFreqMap = (1list) =>
list.reduce((2acc,3cur) =>
({ ...acc, [cur]: (acc[cur] ?? 0) + 1 }), {})
1
Parameter 'list' implicitly has an 'any' type.
2
Parameter 'acc' implicitly has an 'any' type.
3
Parameter 'cur' implicitly has an 'any' type.

So lets add a type annotation.

1
We add the type annotation to the list to get rid of the first three errors
2
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
No index signature with a parameter of type 'string' was found on type '{}'.
TS
contains errors
const getFreqMap = (list: 1string[]) =>
list.reduce((acc,cur) =>
({ ...acc, [cur]: (2acc[cur] ?? 0) + 1 }), {})
1
We add the type annotation to the list to get rid of the first three errors
2
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
No index signature with a parameter of type 'string' was found on type '{}'.

Looks like we need to tell typescript that the type of the second argument of reduce is a an object mapping type string to type number.
We can do this using as .

1
Tells the compiler the type of our initial reduce value.
TS
const getFreqMap = (list: string[]) =>
list.reduce(
(acc, cur) => ({ ...acc, [cur]: (acc[cur] ?? 0) + 1 }),
{} 1as { [key: string]: number }
)
1
Tells the compiler the type of our initial reduce value.

And this works just fine.
But.. Wouldn't it be nice if it worked for any kind of list?
So lets do that then...
Using generics of course.

1
Introducing a generic type
2
An index signature parameter type cannot be a literal type or generic type.
Consider using a mapped object type instead.
3
A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
4
Type 'A' cannot be used to index type '{}'.
TS
contains errors
const getFreqMap = <1A>(list: A[]) =>
list.reduce(
(acc, cur) => ({ ...acc, 3[cur]: (4acc[cur] ?? 0) + 1 }),
{} as { [2key: A]: number }
)
1
Introducing a generic type
2
An index signature parameter type cannot be a literal type or generic type.
Consider using a mapped object type instead.
3
A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
4
Type 'A' cannot be used to index type '{}'.

So we just replaced string with a generic type A.
But alas this is not correct...
We get an error saying we can't use something with type A as an index on {}.
So lets see about the Record type instead.

1
Use the built-in Record type.
2
A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
TS
contains errors
const getFreqMap = <A>(list: A[]) =>
list.reduce(
(acc, cur) => ({ ...acc, 2[cur]: (acc[cur] ?? 0) + 1 }),
{} as1Record<A, number>
)
1
Use the built-in Record type.
2
A computed property name must be of type 'string', 'number', 'symbol', or 'any'.

Omfg another error, srsly???

Oh. Looks like our generic type is a little too generic..
Lets take a look at the type definition for the Record type.

TS
type Record<K extends keyof any, T> = {
[P in K]: T;
};

It says that the type of K (the key of the Record) must be a key of something.
Which gets reduced to string, number, symbol or any as stated by the error message above.
Ok. So lets just add that type constraint to A.

1
Add constraint to the definition of A
TS
const getFreqMap = <A 1extends keyof any>(list: A[]) =>
list.reduce(
(acc, cur) => ({ ...acc, [cur]: (acc[cur] ?? 0) + 1 }),
{} as Record<A, number>
)
1
Add constraint to the definition of A

And here is the finished product!
How beautiful!

But wait, what if we had a list of something more complicated, like a list of objects?
An exercise for the reader perhaps...

Subscribe

Webmentions