Jacob Paris
← Back to all content

Reverse a Record in Typescript

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

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)
}
Professional headshot

Hi, I'm Jacob

Hey there! I'm a developer, designer, and digital nomad with a background in lean manufacturing.

About once per month, I send an email with new guides, new blog posts, and sneak peeks of what's coming next.

Everyone who subscribes gets access to the source code for this website and every example project for all my tutorials.

Stay up to date with everything I'm working on by entering your email below.

Unsubscribe at any time.