Reverse a Record in Typescript

Last updated October 1, 2021 by Jacob Paris

Imagine an adapter that converts the names of countries into their country codes

type CountryName =
| 'Canada'
| 'United States'
| 'Mexico'
type CountryCode = 'CA' | 'US' | 'MX'
const countryNameToCode: Record<
CountryName,
CountryCode
> = {
Canada: 'CA',
'United States': 'US',
Mexico: 'MX',
}

I wanted to also have the reverse adapter, but didn't want to have to manually update it every time it changed. Historically the number of countries are reasonably stable and shouldn't be expected to change often, but this pattern will be used elsewhere too.

Since countryNameToCode is a typed Record, I know that countryNameToCode['Canada'] will output a CountryCode string, and the Typescript engine will enforce that.

The quick function to reverse such a record is to split it into key/value pairs, reverse the pair, and turn back into an object

function reverseRecord(input) {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => [
value,
key,
]),
)
}

Unfortunately that also strips the type information from the record.

reverseRecord(countryNameToCode)
// function reverseRecord(input: any): any

Solution

The Typescript solution is to use type parameters to track the input type, and then cast the result as a Record with those reversed

We'll end up with a signature looking like this

reverseRecord(countryNameToCode)
function reverseRecord<CountryName, CountryCode>(
input: Record<CountryName, CountryCode>,
): Record<CountryCode, CountryName>

Typescript

function reverseRecord<
T extends PropertyKey,
U extends PropertyKey
>(input: Record<T, U>) {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => [
value,
key,
]),
) as Record<U, T>
}

Thanks to Rupert for this solution and thanks to Eddy for the idea to use the built in PropertyKey

JSDoc

I was unable to translate the as Record<U, T> to JSDoc, and passing the output of entries directly into fromEntries wasn't playing well.

// ! Type 'Record<any, string>' is not assignable to type 'Record<U, T>'
function reverseRecord(input) {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => [
value,
key,
]),
) // This does not work
}

For reasons I don't yet understand, splitting it into two lines does seem to fix this.

/**
* @template {PropertyKey} T
* @template {PropertyKey} U
*
* @param {Record<T, U>} input
* @returns {Record<U, T>}
*/
function reverseRecord(input) {
const entries = Object.entries(
input,
).map(([key, value]) => [value, key])
return Object.fromEntries(entries)
}