Yuzhe's Blog

yuzhes

Flip

Flip

Challenge Link

Challenge

Implement the type Flip<T> that takes an object type and returns the “flipped” version — keys become values and values become keys.

Flip<{ a: 'x', b: 'y', c: 'z' }>
// { x: 'a', y: 'b', z: 'c' }

Flip<{ a: 1, b: 2, c: 3 }>
// { 1: 'a', 2: 'b', 3: 'c' }

Flip<{ a: false, b: true }>
// { false: 'a', true: 'b' }

Solution

type Flip<T extends Record<string, any>> = {
  [K in keyof T as T[K] extends PropertyKey ? T[K] : `${T[K]}`]: K
}

Analysis

The key idea is using key remapping (as clause) inside a mapped type to swap keys and values.

Mapped type with key remapping

type Flip<T> = {
  [K in keyof T as T[K]]: K
}

Here K iterates over the keys of T, but the as T[K] clause remaps the key to the corresponding value. The value side K then becomes the original key name.

The problem is that TypeScript requires object keys to be PropertyKey (i.e., string | number | symbol). The values of T might be booleans (true/false), which aren’t directly PropertyKey.

Handling non-PropertyKey values

type Flip<T extends Record<string, any>> = {
  [K in keyof T as T[K] extends PropertyKey ? T[K] : `${T[K]}`]: K
}

We use a conditional type to check: if T[K] is already a valid key type, use it directly. Otherwise, template-literal coerce it to a string. This handles:

Why template literals work for booleans

type BoolToStr = `${true}`  // "true"
type BoolToStr2 = `${false}` // "false"

TypeScript’s template literal types support interpolating boolean, number, bigint, null, and undefined into strings — not just string. This makes \${T[K]}“ a reliable fallback for any non-PropertyKey primitive.

Alternative Approach: Explicit constraint on values

type Flip<T extends Record<string, string | number | boolean>> = {
  [K in keyof T as `${T[K]}`]: K
}

If we constrain T to only allow string | number | boolean values, we can simply always use the template literal coercion. This is simpler and works because \{string}\`` = `string`, `\`{number}`is a numeric string, and`${boolean}`=”true” | “false”`.

The trade-off: this approach converts all number keys to their string forms (e.g., 1 becomes "1"), which is slightly less precise than the original.

Edge Cases

// String values → string keys
type T1 = Flip<{ a: 'hello' }>
// { hello: 'a' } ✓

// Number values → number keys (PropertyKey supports numbers)
type T2 = Flip<{ a: 1, b: 2 }>
// { 1: 'a', 2: 'b' } ✓

// Boolean values → string keys "true"/"false"
type T3 = Flip<{ a: true, b: false }>
// { true: 'a', false: 'b' } ✓

// Value collision: last writer wins (TypeScript deduplicates)
type T4 = Flip<{ a: 'x', b: 'x' }>
// { x: 'b' } (implementation-dependent, usually last key wins)

Key Takeaways