Jacob Paris
← Back to all content

Reverse a Record in Typescript

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

ts
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

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

Unfortunately that also strips the type information from the record.

js
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

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

Typescript

ts
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>
}

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.

js
// ! 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.

js
/**
* @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)
}
Professional headshot
Moulton
Moulton

Hey there! I'm a developer, designer, and digital nomad building cool things with Remix, and I'm also writing Moulton, the Remix Community Newsletter

About once per month, I send an email with:

  • New guides and tutorials
  • Upcoming talks, meetups, and events
  • Cool new libraries and packages
  • What's new in the latest versions of Remix

Stay up to date with everything in the Remix community by entering your email below.

Unsubscribe at any time.