Brand an object and every nested object with its own identity while staying mutually assignable with the original. Nominal typing built by hand.
Nominal typing built by hand: every node of an object tree gets its own identity, and assignability with the original survives.
TypeScript has a structural type system, but sometimes you want a function to accept only certain previously defined objects, not any object that happens to have the required fields. DeepObjectToUniq<O> makes an object and all its deeply nested objects unique while preserving every key and value. The catch: the original type and the unique type must be mutually assignable yet not identical, and two nested objects with the same shape must end up different from each other. The branding technique you'll use here is the same one behind real-world nominal typing libraries.
For example,
import { Equal } from "@type-challenges/utils"
type Quz = { quz: 4 }
type Foo = { foo: 2; baz: Quz; bar: Quz }
type UniqFoo = DeepObjectToUniq<Foo>
declare let foo: Foo
declare let uniqFoo: UniqFoo
uniqFoo = foo // ok
foo = uniqFoo // ok
type T0 = Equal<UniqFoo, Foo> // false
type T1 = UniqFoo["foo"] // 2
type T2 = Equal<UniqFoo["bar"], UniqFoo["baz"]> // false
type T3 = UniqFoo["bar"]["quz"] // 4
type T4 = Equal<keyof Foo & string, keyof UniqFoo & string> // trueTypeScript has structural type system, but sometimes you want a function to accept only some previously well-defined unique objects (as in the nominal type system), and not any objects that have the required fields.
Create a type that takes an object and makes it and all deeply nested objects in it unique, while preserving the string and numeric keys of all objects, and the values of all properties on these keys.
The original type and the resulting unique type must be mutually assignable, but not identical.
For example,
import { Equal } from "@type-challenges/utils"
type Foo = { foo: 2; bar: { 0: 1 }; baz: { 0: 1 } }
type UniqFoo = DeepObjectToUniq<Foo>
declare let foo: Foo
declare let uniqFoo: UniqFoo
uniqFoo = foo // ok
foo = uniqFoo // ok
type T0 = Equal<UniqFoo, Foo> // false
type T1 = UniqFoo["foo"] // 2
type T2 = Equal<UniqFoo["bar"], UniqFoo["baz"]> // false
type T3 = UniqFoo["bar"][0] // 1
type T4 = Equal<keyof Foo & string, keyof UniqFoo & string> // trueView on GitHub: https://tsch.js.org/553
Change the following code to make the test cases pass (no type check errors).
👋 Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
All of it fits in one declaration and one type:
declare const KEY: unique symbol
type DeepObjectToUniq<
O extends object,
Root = O,
Path extends PropertyKey[] = [],
> = {
[K in keyof O]: O[K] extends object
? DeepObjectToUniq<O[K], Root, [...Path, K]>
: O[K]
} & { readonly [KEY]?: [Root, Path] }A phantom brand property makes the type distinguishable from the original. Its optional modifier keeps assignability intact, and a path accumulator threaded through the recursion makes every nesting level unique. One piece at a time.
unique symboldeclare const KEY: unique symbol declares a symbol whose type is unlike any other; even another unique symbol is a different type. The declare const is load-bearing: a computed property key must be a value, not a type, so we need a constant whose type is a unique symbol, and declare makes it exist at the type level only, emitting no runtime code. Intersecting the object with { readonly [KEY]?: [Root, Path] } attaches a phantom property keyed by that symbol. This is the classic branding trick: the property never exists at runtime, but at the type level it makes UniqFoo distinguishable from Foo, so Equal<UniqFoo, Foo> is false.
Using a symbol key rather than a string one is what keeps the last example true: keyof UniqFoo is keyof Foo | typeof KEY, and intersecting with string filters the symbol out, so keyof Foo & string and keyof UniqFoo & string agree. The string and numeric keys are preserved untouched.
The challenge demands mutual assignability: uniqFoo = foo and foo = uniqFoo must both compile.
foo = uniqFoo works regardless. uniqFoo has every property Foo wants, plus one extra, and extra properties are fine structurally.uniqFoo = foo is the direction that would break with a required brand: foo has no [KEY] property. Making the brand optional (?) means its absence is acceptable, so plain Foo values flow in freely.The readonly modifier is a small courtesy: nobody should ever write to a property that doesn't exist at runtime.
Branding only the top level isn't enough. Here are the test file's definitions in full, the same Quz and Foo as in the intro plus a second root type Bar:
type Quz = { quz: 4 }
type Foo = { foo: 2; baz: Quz; bar: Quz }
type Bar = { foo: 2; baz: Quz; bar: Quz & { quzz?: 0 } }In Foo, bar and baz are the same type, yet Equal<UniqFoo['bar'], UniqFoo['baz']> must be false. If the recursion were plain DeepObjectToUniq<O[K]>, both would evaluate to DeepObjectToUniq<Quz>, which is identical. The fix is to thread two extra parameters through the recursion:
Root: the original top-level object, fixed at the first call via the default Root = O and passed down unchanged.Path: a tuple of the keys walked so far, extended at each step with [...Path, K].Each nested object's brand is then [Root, Path], a fingerprint of where it lives:
// UniqFoo['bar'] is branded with [Foo, ['bar']]
// UniqFoo['baz'] is branded with [Foo, ['baz']] → different from 'bar'
// UniqQuz is branded with [Quz, []] → different from bothIncluding Root matters too. Foo and Bar in the tests both have a baz: Quz property, so their paths match, but their roots differ. That is why Equal<UniqBar['baz'], UniqFoo['baz']> is false.
The main body is a homomorphic mapped type: [K in keyof O] copies every property, and the conditional O[K] extends object ? ... : O[K] decides whether to recurse. Primitives like foo: 2 pass through untouched, which is why UniqFoo['foo'] is still exactly 2 and UniqFoo['bar']['quz'] is still 4. Only object-valued properties get wrapped in another branded layer.
IsTrue<Equal<keyof UniqBar['baz'], keyof UniqFoo['baz']>>: both nested brands use the same KEY symbol, so their key sets agree even though the branded types themselves differ. One shared unique symbol for all levels is essential; an inline symbol type per level would break this.Bar's bar: Quz & { quzz?: 0 }: intersections are objects too, so the recursion descends into them and brands them like any other nested object.Branding via an optional unique symbol property is a technique you'll meet in production code, for opaque IDs and validated strings. This challenge shows how far it can be pushed: full nominal identity for every node of an object tree.
This challenge is originally from here.