Flip
Flip
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:
string→ used as-is ✓number→ used as-is (numbers are valid PropertyKey) ✓boolean(true/false) → coerced to"true"/"false"✓
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
- Key remapping with
asis the central tool for transforming mapped type keys T[K] extends PropertyKeyguards against invalid key types- Template literal types (
\${T[K]}`) can coerceboolean,number`, etc. to their string representations - When values collide (two keys map to the same new key), TypeScript keeps one — behavior depends on the order of iteration
