TypeScript NotesRohit’s interview study guide
Chapter 14

Type Challenges

The "implement this type" questions from the notes, solved step by step.

8 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#DeepPartial

The challenge: write DeepPartial<T> — like Partial<T>, but every nested object's properties become optional too, all the way down. My notes say this one "broke me", and that a custom deep partial "will break you 9000 times". It's worth seeing exactly why the obvious version breaks, because the fix teaches the general recipe for every deep type.

The idea: Partial is a mapped type that adds ? to the top level only. To go deeper, the value type of each property has to be DeepPartial of the original value type — a recursive type. The hard part isn't the recursion; it's deciding what to do with things that aren't plain objects: primitives, arrays, functions, and built-ins like Date.

The version everyone writes first:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type DeepPartialNaive<T> = { [K in keyof T]?: DeepPartialNaive<T[K]> };

interface Settings {
  theme: { color: string; fontSize: number };
  tags: string[];
  onSave: (s: string) => void;
}

type N = DeepPartialNaive<Settings>;

type T1 = Expect<Equal<N["theme"], { color?: string; fontSize?: number } | undefined>>; // good
type T2 = Expect<Equal<N["tags"], (string | undefined)[] | undefined>>;                // holes allowed
type T3 = Expect<Equal<N["onSave"], {} | undefined>>;                                  // not a function any more!

const oops: N = { onSave: {}, tags: [undefined] }; // compiles
What’s happening
  1. DeepPartialNaive<Settings> loops over "theme" | "tags" | "onSave", adds ? to each, and recurses on each value type. (| undefined appears in the assertions because reading an optional property gives X | undefined.)
  2. theme: DeepPartialNaive<{ color: string; fontSize: number }> recurses once more and makes both optional. Then DeepPartialNaive<string> hits a primitive, and a homomorphic mapped type returns primitives unchanged — so recursion stops by itself. This part works.
  3. tags: DeepPartialNaive<string[]> is a homomorphic mapped type applied to an array, so it maps the elements — and the ? makes each element optional, giving (string | undefined)[]. Now [undefined] is a valid tag list. Not what anyone meant.
  4. onSave: a function type has no properties for keyof to find, so the mapped type produces {} — an empty object type, which accepts almost anything ({}, 5, "x"). The callback type is simply gone, which is why onSave: {} compiles.
  5. So the naive version is right for plain objects and wrong for the two most common non-plain values. The fix is to branch on what kind of type we have before mapping.
AdvancedDeepPartial solution

The solution — check functions first, then arrays, then objects, then fall through for primitives:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type DeepPartial<T> =
  T extends (...args: any[]) => unknown ? T
  : T extends readonly unknown[] ? { [K in keyof T]: DeepPartial<T[K]> }
  : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Settings {
  theme: { color: string; fontSize: number };
  tags: string[];
  onSave: (s: string) => void;
  servers: { host: string; port: number }[];
}

type T1 = Expect<Equal<DeepPartial<Settings>, {
  theme?: { color?: string; fontSize?: number };
  tags?: string[];
  onSave?: (s: string) => void;
  servers?: { host?: string; port?: number }[];
}>>;
type T2 = Expect<Equal<DeepPartial<[{ a: 1 }, string]>, [{ a?: 1 }, string]>>; // tuples keep their shape
type T3 = Expect<Equal<DeepPartial<string | null>, string | null>>;

const patch: DeepPartial<Settings> = { theme: { color: "red" }, servers: [{ port: 8080 }] };
const broken: DeepPartial<Settings> = { onSave: {} }; // ❌ TS2322: Type '{}' is not assignable to type '(s: string) => void'.
What’s happening
  1. Top level, T = Settings: not a function, not an array, but it is an object, so the third branch maps every key with ? and recurses on each value. Now each property is handled on its own.
  2. theme → DeepPartial<{ color: string; fontSize: number }> → object branch again → { color?: …; fontSize?: … }. Then DeepPartial<string>: not a function, not an array, not an object → last branch returns string. That's the base case that ends the recursion.
  3. tags → DeepPartial<string[]> hits the array branch. It maps the array with a homomorphic mapped type but without ?, so the result is still an array (string[]) with each element passed through DeepPartial (string stays string). No undefined holes.
  4. onSave hits the first branch and is returned as is — the function type survives, so onSave: {} now fails with "TS2322: Type '{}' is not assignable to type '(s: string) => void'." The function check must come before the object check, because functions are objects too.
  5. servers → array branch → each element { host: string; port: number } → object branch → { host?: string; port?: number }. So [{ port: 8080 }] is valid: the array is required to be an array, but each server can be partial. Because the array branch maps over keyof T, tuples keep their exact shape too (T2).
  6. T is a naked type parameter in every extends, so the whole thing distributes over unions: DeepPartial<Address | null> handles Address and null separately — T3 shows string | null passing through untouched.
AdvancedWhere DeepPartial is useful

Where you'd use it: PATCH-style update payloads for nested resources, test fixtures that override a few nested fields of a big default object, and config merging (merge(defaults, userConfig: DeepPartial<Config>)).

AdvancedPitfall: Date and Map in deep mapping

#DeepReadonly

The challenge: make every property readonly at every level, including arrays inside, so that nothing reachable from the root can be assigned or mutated. Readonly<T> only does the top level — my notes call the deep version "Recursion Deep (make all properties ReadOnly)".

The structure is the same as DeepPartial: recurse through objects, stop at primitives, leave functions alone. Arrays need less special handling this time, because a homomorphic mapped type with readonly applied to an array produces a readonly array — exactly what we want.

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type DeepReadonly<T> =
  T extends (...args: any[]) => unknown ? T
  : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface Config {
  server: { host: string; ports: number[] };
  log: (message: string) => void;
}

type T1 = Expect<Equal<DeepReadonly<Config>, {
  readonly server: { readonly host: string; readonly ports: readonly number[] };
  readonly log: (message: string) => void;
}>>;
type T2 = Expect<Equal<DeepReadonly<[1, { a: 2 }]>, readonly [1, { readonly a: 2 }]>>;

declare const config: DeepReadonly<Config>;
config.server.host = "x"; // ❌ TS2540: Cannot assign to 'host' because it is a read-only property.
config.server.ports.push(1); // ❌ TS2339: Property 'push' does not exist on type 'readonly number[]'.
config.server.ports[0] = 2; // ❌ TS2542: Index signature in type 'readonly number[]' only permits reading.
config.log("still callable");
What’s happening
  1. DeepReadonly<Config>: Config isn't a function, it's an object, so every key gets readonly and its value recurses: readonly server: DeepReadonly<…>, readonly log: DeepReadonly<(message: string) => void>.
  2. server → object branch → readonly host: DeepReadonly<string> = string (base case), and readonly ports: DeepReadonly<number[]>.
  3. number[] is an object, so it takes the same mapping branch — but a homomorphic mapped type over an array produces an array, and readonly on the mapping makes it a readonly number[]. That's why push doesn't exist ("TS2339: Property 'push' does not exist on type 'readonly number[]'.") and index writes are rejected ("TS2542: Index signature in type 'readonly number[]' only permits reading."). Tuples become readonly tuples (T2).
  4. log hits the function branch and is returned untouched. Without that branch it would be mapped like an object, lose its call signature, and config.log(…) would fail with "TS2349: This expression is not callable."
  5. config.server.host = "x" fails two levels deep with "TS2540: Cannot assign to 'host' because it is a read-only property." — the error the shallow Readonly can't give you.
AdvancedRuntime caveat for DeepReadonly

#PartialBy: make some properties optional

The challenge: PartialBy<T, K> makes only the keys in K optional and leaves everything else exactly as it was. Typical use: a "create" form where id and createdAt are optional because the server fills them in. My notes called this "very nice and important".

The idea is to split, transform, recombine: pull the chosen keys out with Pick, make that piece Partial, take everything else with Omit, and intersect the two halves back together.

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
}

type PartialByRaw<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type Prettify<T> = { [K in keyof T]: T[K] };
type PartialBy<T, K extends keyof T> = Prettify<Omit<T, K> & Partial<Pick<T, K>>>;

type Flat = { id: number; name: string; age?: number; email?: string };
type T1 = Expect<Equal<PartialByRaw<User, "email">, Flat>>; // ❌ TS2344: Type 'false' does not satisfy the constraint 'true'.
type T2 = Expect<Equal<PartialBy<User, "email">, Flat>>;

const draft: PartialBy<User, "email"> = { id: 1, name: "Ada" };
const nope: PartialBy<User, "name"> = { id: 1, email: "a@b.c" }; // fine — name is optional now
const typo: PartialBy<User, "emial"> = { id: 1, name: "Ada" }; // ❌ TS2344: Type '"emial"' does not satisfy the constraint 'keyof User'.
What’s happening
  1. Pick<User, "email"> = { email: string }, and Partial of that is { email?: string } — the piece we want to change.
  2. Omit<User, "email"> = { id: number; name: string; age?: number } — everything else, modifiers intact (age stays optional because Omit goes through Pick, which is homomorphic; see Rebuilding Pick, Required and Omit).
  3. The intersection { id: number; name: string; age?: number } & { email?: string } already behaves correctly: draft without email is accepted. But it's still an intersection, so editor hovers show Omit<User, "email"> & Partial<Pick<User, "email">>, and the strict Equal test treats it as different from the flat object — that's the ❌ on T1.
  4. Prettify is the identity mapped type. Mapping over keyof (A & B) visits every key of both halves, copies each property with its modifiers, and produces one flat object type: { id: number; name: string; age?: number; email?: string }. Same values allowed, much nicer to read — so T2 passes.
  5. K extends keyof T catches typos at the use site: "emial" fails with "TS2344: Type '"emial"' does not satisfy the constraint 'keyof User'." The built-in Omit alone wouldn't catch it.
AdvancedPrettify and the OR-undefined alternative

You'll often see Prettify written with & {} on the end; that's a hint for editor hovers and doesn't change the type. The mirror image is RequiredBy<T, K> = Prettify<Omit<T, K> & Required<Pick<T, K>>>.

My notes also had "another smart way of doing partial optional — do an OR with undefined". The screenshot is gone, so here is what that idea gives you — it's close, but not the same:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
}

type OrUndefined<T, K extends keyof T> = { [P in keyof T]: P extends K ? T[P] | undefined : T[P] };

type T1 = Expect<Equal<OrUndefined<User, "email">, { id: number; name: string; email: string | undefined; age?: number }>>;

const explicit: OrUndefined<User, "email"> = { id: 1, name: "Ada", email: undefined }; // fine
const omitted: OrUndefined<User, "email"> = { id: 1, name: "Ada" }; // ❌ TS2741: Property 'email' is missing in type '{ id: number; name: string; }' but required in type 'OrUndefined<User, "email">'.
What’s happening
  1. One mapped type over keyof User — homomorphic, so age? keeps its ?. For each key, a conditional asks "is this one of the keys in K?" (the type-level === is extends).
  2. P = "email": yes, so the value becomes string | undefined. Every other key keeps its type.
  3. But the ? modifier hasn't been added — only the value type has changed. So email is still required: you must write it, though you may write undefined. omitted fails with TS2741.
  4. "Optional" (email?: string, may be absent) and "possibly undefined" (email: string | undefined, must be present) are different contracts. The second is useful when you want callers to consciously pass undefined — e.g. a React prop that must be specified, even if empty.
  5. To get real optionality from a single mapped type, you need to add ? per key, which a mapped type can't do conditionally — hence the split-and-intersect solution above (or two mapped types with an as filter each, intersected and prettified).

#Make optional properties nullable

The challenge from my notes ("Excellent usage of PICK — Question/Answer"): given a type, allow null on every property that is optional, and leave required properties alone. The doc's answer:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type NullableTask<T> = { [P in keyof T]: {} extends Pick<T, P> ? T[P] | null : T[P] };

interface Task {
  id: number;
  title: string;
  notes?: string;
  due?: Date;
  assignee: string | undefined; // required, but may hold undefined
}

type T1 = Expect<Equal<NullableTask<Task>, {
  id: number;
  title: string;
  notes?: string | null;
  due?: Date | null;
  assignee: string | undefined;
}>>;

type T2 = Expect<Equal<Pick<Task, "notes">, { notes?: string }>>;
type T3 = Expect<Equal<{} extends Pick<Task, "notes"> ? true : false, true>>;
type T4 = Expect<Equal<{} extends Pick<Task, "id"> ? true : false, false>>;
type T5 = Expect<Equal<{} extends Pick<Task, "assignee"> ? true : false, false>>;
AdvancedDetecting optional properties with Pick

The idea: a mapped type can't directly ask "is this property optional?" — T[P] gives you the value type, not the modifier. So the trick turns the question into an assignability test. Pick<T, P> builds a one-property object type that keeps P's modifier. Then ask: is the empty object {} assignable to it? An object with no properties satisfies { notes?: string } (the property may be absent) but not { id: number } (the property must be there). So {} extends Pick<T, P> is true exactly when P is optional.

What’s happening
  1. The mapped type loops over keyof Task = "id" | "title" | "notes" | "due" | "assignee". It's homomorphic, so notes? and due? keep their ? whichever branch is taken.
  2. P = "id": Pick<Task, "id"> is { id: number }. Is {} assignable to it? No — id is required and {} doesn't have it. False branch: id: number. Same for title.
  3. P = "notes": Pick<Task, "notes"> is { notes?: string } (T2). Is {} assignable? Yes — a missing optional property is fine. True branch: T["notes"] | null = string | undefined | null, displayed as notes?: string | null (the undefined is implied by ?). Same for due.
  4. P = "assignee": this one is required but its type includes undefined. Pick gives { assignee: string | undefined }, and {} isn't assignable — the key must be present even if its value is undefined. So it's left alone (T5). That's the correct answer: the challenge is about the ? modifier, not about undefined.
  5. Why Pick and not T[P]? T[P] throws away the modifier. Pick is homomorphic, so it's the simplest way to carry one property with its modifier into a type you can test.
AdvancedComputing a type's optional keys

The same trick is the standard way to compute the optional keys of a type — often a sub-question in interviews:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;

interface Task {
  id: number;
  notes?: string;
  due?: Date;
}

type T1 = Expect<Equal<OptionalKeys<Task>, "notes" | "due">>;
type T2 = Expect<Equal<RequiredKeys<Task>, "id">>;
What’s happening
  1. The mapped type produces, for each key, either the key itself (optional) or never (required): { id: never; notes: "notes"; due: "due" }.
  2. The -? matters: without it, notes and due would stay optional in this intermediate object, and reading them would add undefined to the result.
  3. [keyof T] at the end indexes that object with all its keys, collecting every value into a union: never | "notes" | "due" = "notes" | "due". This "map then index" pattern is [Getting every value type with T[keyof T]](#getting-every-value-type-with-t-keyof-t) applied to computed values.
  4. RequiredKeys is just the rest: Exclude<"id" | "notes" | "due", "notes" | "due"> = "id".
Why not undefined extends T[P]?

A shorter-looking answer to the challenge is { [P in keyof T]: undefined extends T[P] ? T[P] | null : T[P] }. What does it do to Task's assignee: string | undefined?

Show answer

It makes it string | undefined | null — wrong, because assignee is required.

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type ByUndefined<T> = { [P in keyof T]: undefined extends T[P] ? T[P] | null : T[P] };

interface Task {
  id: number;
  notes?: string;
  assignee: string | undefined;
}

type T1 = Expect<Equal<ByUndefined<Task>, { id: number; notes?: string | null; assignee: string | undefined | null }>>;
What’s happening
  1. T["notes"] is string | undefined because reading an optional property may give undefined. undefined extends string | undefined is true, so notes gets | null — correct so far.
  2. T["assignee"] is also string | undefined, because that's how it was declared. The test can't tell the two apart: indexed access only sees value types, not modifiers.
  3. So assignee gets | null too, even though it's a required property. The {} extends Pick<T, P> version looks at the property with its modifier and gets it right.

#Reverse a tuple

The challenge from my notes: [number, string, boolean] should become [boolean, string, number]. The doc's solution:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Reverse<T> = T extends [infer First, ...infer Rest] ? [...Reverse<Rest>, First] : T;

type OriginalTuple = [number, string, boolean];
type Reversed = Reverse<OriginalTuple>;

const fff: Reversed = [true, "2", 1];
const wrong: Reversed = [1, "2", true]; // ❌ TS2322: Type 'number' is not assignable to type 'boolean'.

type T1 = Expect<Equal<Reversed, [boolean, string, number]>>;
type T2 = Expect<Equal<Reverse<[1, 2, 3]>, [3, 2, 1]>>;
type T3 = Expect<Equal<Reverse<[]>, []>>;
type T4 = Expect<Equal<Reverse<[1]>, [1]>>;
AdvancedRecursive reverse step by step

The idea is the classic recursive reverse from any functional language: reverse(list) = reverse(tail) + head. infer with a variadic tuple pattern [infer First, ...infer Rest] splits the tuple into its first element and the rest, exactly like const [first, ...rest] = arr. The recursion stops when the tuple is empty, because [] can't match a pattern that needs at least one element.

What’s happening
  1. Reverse<[1, 2, 3]>: does [1, 2, 3] match [infer First, ...infer Rest]? Yes — First = 1, Rest = [2, 3]. Result: [...Reverse<[2, 3]>, 1], which can't be built until the inner call finishes.
  2. Reverse<[2, 3]>: First = 2, Rest = [3]. Result: [...Reverse<[3]>, 2].
  3. Reverse<[3]>: First = 3, Rest = []. Result: [...Reverse<[]>, 3].
  4. Reverse<[]>: an empty tuple has no first element, so the match fails and the false branch returns T itself — []. That's the base case.
  5. Now the pending results unwind, innermost first: [...[], 3] = [3]; then [...[3], 2] = [3, 2]; then [...[3, 2], 1] = [3, 2, 1].
  6. For the doc's tuple the same steps give [boolean, string, number], so [true, "2", 1] fits, and [1, "2", true] fails position by position — the first error is "TS2322: Type 'number' is not assignable to type 'boolean'."
AdvancedEdge cases for Reverse

The edge cases are where interviewers push:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Reverse<T> = T extends [infer First, ...infer Rest] ? [...Reverse<Rest>, First] : T;

type T1 = Expect<Equal<Reverse<number[]>, number[]>>;                  // not a tuple: unchanged
type T2 = Expect<Equal<Reverse<[1, ...string[]]>, [...string[], 1]>>;  // variadic: still right
type T3 = Expect<Equal<Reverse<readonly [1, 2]>, readonly [1, 2]>>;    // readonly: silently unchanged!

type ReverseFixed<T extends readonly unknown[]> =
  T extends readonly [infer First, ...infer Rest] ? [...ReverseFixed<Rest>, First] : [];

type T4 = Expect<Equal<ReverseFixed<readonly [1, 2]>, [2, 1]>>;
type T5 = Expect<Equal<ReverseFixed<[1, 2, 3]>, [3, 2, 1]>>;
What’s happening
  1. Reverse<number[]>: an array of unknown length has no guaranteed first element, so it doesn't match [infer First, ...infer Rest] and comes back unchanged. That's the right answer — reversing a number[] gives a number[].
  2. Reverse<[1, ...string[]]>: it does have a first element, so First = 1 and Rest = string[]. Reverse<string[]> is string[] (step 1), and the result is [...string[], 1] — a tuple that ends with 1. Correct, and a nice thing to point out.
  3. Reverse<readonly [1, 2]>: a readonly tuple isn't assignable to the mutable pattern [infer First, ...infer Rest], so the match fails and the input comes back unreversed. This is the real bug in the doc's version — it bites with any as const tuple.
  4. ReverseFixed uses a readonly […] pattern (which matches both mutable and readonly tuples), constrains T to arrays so nonsense like Reverse<string> is rejected, and returns [] as the base case. T4 and T5 show both kinds now work.
  5. Both versions recurse once per element and aren't tail-recursive, so very long tuples hit the depth limit; Recursive types shows the accumulator version that handles ~1,000 elements.
Predict the result
TypeScript
type Reverse<T> = T extends [infer First, ...infer Rest] ? [...Reverse<Rest>, First] : T;
const point = [10, 20, 30] as const;
type R = Reverse<typeof point>;
Show answer

readonly [10, 20, 30] — unchanged.

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Reverse<T> = T extends [infer First, ...infer Rest] ? [...Reverse<Rest>, First] : T;
const point = [10, 20, 30] as const;

type T1 = Expect<Equal<Reverse<typeof point>, readonly [10, 20, 30]>>;
What’s happening
  1. as const makes typeof point the readonly tuple readonly [10, 20, 30], with literal element types.
  2. The pattern [infer First, ...infer Rest] describes a mutable tuple. A readonly tuple lacks the mutating methods a mutable one has, so it isn't assignable, and the match fails on the very first step.
  3. The false branch returns T: the original tuple, unreversed, with no error to warn you. Writing the pattern as readonly [infer First, ...infer Rest] fixes it and gives [30, 20, 10].

#Merge two object types

The challenge: Merge<A, B> combines two object types, and when both have the same key, B's property wins — the type-level version of { ...a, ...b }. My notes have three entries on this ("Merge 2 objects", "Merge object types", "Another mind bender"), and the mind-bending part is why the obvious answer, A & B, is wrong.

An intersection doesn't override; it requires a value to satisfy both types at once. For a shared key with different types, the property's type becomes the intersection of the two, which is often impossible:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type A = { id: number; name: string; tags: string[] };
type B = { id: string; active: boolean };

type Both = A & B;
type T1 = Expect<Equal<Both["id"], never>>; // number & string

const x: Both = { id: "1", name: "Ada", tags: [], active: true }; // ❌ TS2322: Type 'string' is not assignable to type 'never'.

type Merge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never;
};

type T2 = Expect<Equal<Merge<A, B>, { id: string; name: string; tags: string[]; active: boolean }>>;
const y: Merge<A, B> = { id: "1", name: "Ada", tags: [], active: true };
What’s happening
  1. Both["id"] is number & string. No value is both a number and a string, so that's never, and no object can ever satisfy Both — x fails with "TS2322: Type 'string' is not assignable to type 'never'." (See extends vs intersection for the interface version of this trap.)
  2. Merge loops over every key from either side: keyof A | keyof B = "id" | "name" | "tags" | "active".
  3. For each key it asks two questions in order. K = "id": is it a key of B? Yes → B["id"] = string. B is checked first, which is what makes B win.
  4. K = "name": not in B; is it in A? Yes → A["name"] = string. Same for tags. K = "active": in B → boolean. The final never branch is unreachable — every key came from one side or the other — but a conditional type needs a false branch.
  5. Result: { id: string; name: string; tags: string[]; active: boolean } — exactly what { ...a, ...b } would produce at runtime.
AdvancedMerge vs Omit plus intersection

There's a second way to write it, and the difference shows up with optional properties:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Prettify<T> = { [K in keyof T]: T[K] };

type Merge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never;
};
type MergeByOmit<A, B> = Prettify<Omit<A, keyof B> & B>;

type A = { id: number; nickname?: string };
type B = { id: string; avatar?: string };

type T1 = Expect<Equal<Merge<A, B>, { id: string; nickname: string | undefined; avatar: string | undefined }>>;
type T2 = Expect<Equal<MergeByOmit<A, B>, { id: string; nickname?: string; avatar?: string }>>;
What’s happening
  1. Merge loops over keyof A | keyof B — a computed union, not keyof of a single type — so it isn't homomorphic and doesn't copy modifiers (the same trap as OmitNaive in Rebuilding Pick, Required and Omit). nickname and avatar become required properties typed string | undefined.
  2. MergeByOmit first removes from A every key that B also has: Omit<A, "id" | "avatar"> = { nickname?: string } (Omit goes through Pick, so ? survives).
  3. Then it intersects with all of B: { nickname?: string } & { id: string; avatar?: string }. There are no shared keys any more, so the intersection can't create never properties — B's version of id is the only one left.
  4. Prettify flattens the intersection into one object, copying modifiers: { id: string; nickname?: string; avatar?: string }. That's the more faithful "spread" type, and the one to give in an interview — then mention the mapped version as the "from scratch" answer.
AdvancedEdge case: spreading optional properties

#Recursive infer: flatten nested arrays

The challenge: flatten a nested tuple into one level, at any depth: Flatten<[1, [2, [3, [4]]], 5]> should be [1, 2, 3, 4, 5]. My notes call recursive infer something that "will break you" — the solution screenshot is gone, so this is the classic version. It combines everything: infer to split the tuple, a conditional to check each element, and recursion in two directions — down into nested tuples and along the rest of the list.

The plan, in words: take the first element. If it's an array, flatten it and spread the result in; if not, keep it as is. Then do the same for the rest. An empty tuple flattens to [].

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Flatten<T extends readonly unknown[]> =
  T extends readonly [infer First, ...infer Rest]
    ? First extends readonly unknown[]
      ? [...Flatten<First>, ...Flatten<Rest>]
      : [First, ...Flatten<Rest>]
    : [];

type T1 = Expect<Equal<Flatten<[1, [2, [3]]]>, [1, 2, 3]>>;
type T2 = Expect<Equal<Flatten<[1, [2, [3, [4]]], 5]>, [1, 2, 3, 4, 5]>>;
type T3 = Expect<Equal<Flatten<[[], [[]]]>, []>>;
type T4 = Expect<Equal<Flatten<readonly [1, readonly [2]]>, [1, 2]>>;
What’s happening
  1. Flatten<[1, [2, [3]]]>: split into First = 1, Rest = [[2, [3]]]. 1 isn't an array, so the result is [1, ...Flatten<[[2, [3]]]>].
  2. Flatten<[[2, [3]]]>: First = [2, [3]], Rest = []. First is an array, so recurse into it: [...Flatten<[2, [3]]>, ...Flatten<[]>].
  3. Flatten<[2, [3]]>: First = 2 → [2, ...Flatten<[[3]]>]. And Flatten<[[3]]>: First = [3] is an array → [...Flatten<[3]>, ...Flatten<[]>], where Flatten<[3]> is [3, ...Flatten<[]>].
  4. Base case: Flatten<[]> doesn't match [infer First, ...infer Rest], so it's []. Unwinding: Flatten<[3]> = [3], Flatten<[[3]]> = [3], Flatten<[2, [3]]> = [2, 3], Flatten<[[2, [3]]]> = [2, 3], and finally [1, ...[2, 3]] = [1, 2, 3].
  5. Empty nested tuples contribute nothing (T3), and the readonly patterns mean as const input works (T4) — the lesson from Reverse a tuple applied up front.
AdvancedFlattening arrays of unknown length

For arrays of unknown length there's nothing to split, so the question becomes "what's the innermost element type?" — recursive infer again, but much simpler:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type DeepElement<T> = T extends readonly (infer E)[] ? DeepElement<E> : T;

type T1 = Expect<Equal<DeepElement<number[][][]>, number>>;
type T2 = Expect<Equal<DeepElement<(string | number[])[]>, string | number>>;
type T3 = Expect<Equal<DeepElement<number[][]>[], number[]>>; // a fully flattened array type

const nested = [1, [2, [3]]];
const flat = nested.flat(Infinity); // built-in: typed with FlatArray<…>
What’s happening
  1. DeepElement<number[][][]>: matches readonly (infer E)[] with E = number[][], recurse; E = number[], recurse; E = number, recurse; number isn't an array → return number. One infer peels one level of nesting.
  2. DeepElement<(string | number[])[]>: E = string | number[]. Now T is a naked type parameter, so the next step distributes: DeepElement<string> | DeepElement<number[]> = string | number. Mixed nesting depths are handled for free.
  3. T3 wraps the result back in [] to get the type of a fully flattened array — number[].
  4. The built-in Array.prototype.flat is typed with FlatArray<Arr, Depth> in lib.es2019.array.d.ts: the same ReadonlyArray<infer InnerArr> peel, plus a depth counter. It counts down by indexing a lookup tuple — [-1, 0, 1, 2, …, 20][Depth] gives Depth - 1 — and stops at -1. Worth reading once.
AdvancedPitfall: Flatten with plain arrays

#Dot-notation paths of an object

Added

The challenge: given a nested object type, produce the union of every valid dot path — Paths<{ a: { b: { c: string } } }> should be "a" | "a.b" | "a.b.c" — and then type a get(obj, path) function so that the path is autocompleted and the return type is the type at that path. This is how form libraries (react-hook-form's register("address.city")), i18n keys and lodash-style get are typed, and it's a favourite "senior" question because it uses mapped types, template literals, recursion and infer together.

The idea in two halves. Generating paths: for each key K, the paths are K itself plus `${K}.${sub-path}` for every path inside T[K]; collect all of that into one union. Resolving a path: split the string at the first dot with infer, step into that property, and recurse on the rest.

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Paths<T> = T extends object
  ? { [K in keyof T & string]: K | `${K}.${Paths<T[K]>}` }[keyof T & string]
  : never;

interface Config {
  db: { host: string; port: number; auth: { user: string } };
  debug: boolean;
}

type T1 = Expect<Equal<Paths<Config>, "db" | "db.host" | "db.port" | "db.auth" | "db.auth.user" | "debug">>;
What’s happening
  1. Paths<Config>: Config is an object, so we build a mapped type over its string keys "db" | "debug" and then index it with those same keys ([keyof T & string]) to collect every value into a union — the "map then index" pattern.
  2. K = "debug": the value is the union of "debug" and `debug.${Paths<boolean>}`. Paths<boolean> is never (not an object), and a template literal with a never placeholder is itself never — there's no string to build. So the value is just "debug". That's the base case: leaves contribute only their own key.
  3. K = "db": the value is the union of "db" and `db.${Paths<Config["db"]>}`. Recursing into { host; port; auth } gives "host" | "port" | "auth" | "auth.user" (the same steps one level down, with auth recursing once more to produce "auth.user").
  4. A template literal with a union placeholder produces one string per member, so `db.${"host" | "port" | "auth" | "auth.user"}` becomes "db.host" | "db.port" | "db.auth" | "db.auth.user".
  5. Indexing the mapped object with "db" | "debug" unions both values: "db" | "db.host" | "db.port" | "db.auth" | "db.auth.user" | "debug".
AdvancedA typed get for dot paths

Now the typed get:

TypeScript
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

type Paths<T> = T extends object
  ? { [K in keyof T & string]: K | `${K}.${Paths<T[K]>}` }[keyof T & string]
  : never;

type PathValue<T, P extends string> =
  P extends `${infer Head}.${infer Rest}`
    ? Head extends keyof T ? PathValue<T[Head], Rest> : never
    : P extends keyof T ? T[P] : never;

function get<T, P extends Paths<T>>(obj: T, path: P): PathValue<T, P> {
  return path.split(".").reduce<any>((value, key) => value?.[key], obj);
}

interface Config {
  db: { host: string; port: number; auth: { user: string } };
  debug: boolean;
}

declare const config: Config;

const port = get(config, "db.port");
const auth = get(config, "db.auth");

type T1 = Expect<Equal<typeof port, number>>;
type T2 = Expect<Equal<typeof auth, { user: string }>>;
type T3 = Expect<Equal<PathValue<Config, "db.auth.user">, string>>;

get(config, "db.pass"); // ❌ TS2345: Argument of type '"db.pass"' is not assignable to parameter of type '"db" | "db.auth" | "db.auth.user" | "db.host" | "db.port" | "debug"'.
What’s happening
  1. get(config, "db.port"): T is inferred as Config from the first argument, so P is constrained to Paths<Config>. The literal "db.port" is in that union, so P = "db.port". In the editor, typing the second argument autocompletes from the union.
  2. The return type is PathValue<Config, "db.port">. The pattern `${infer Head}.${infer Rest}` splits at the first dot (the first placeholder matches as little as possible): Head = "db", Rest = "port". "db" is a key of Config, so recurse: PathValue<Config["db"], "port">.
  3. "port" has no dot, so the second branch: "port" is a key of the db object, giving number. So port: number.
  4. "db.auth" resolves the same way to the whole { user: string } object, and "db.auth.user" takes one more recursive step to string.
  5. "db.pass" isn't in Paths<Config>, so the call is rejected at compile time with TS2345 — and the message lists every valid path, which is a helpful error in practice.
  6. The runtime body splits the path and walks the object with reduce. It's typed any internally — the checker can't prove that a runtime string walk matches the type-level one — and the signature is what gives callers the safety. That's the usual split for typed utilities: precise public types, a small trusted implementation.
AdvancedPitfalls of Paths on real models

Built from Rohit’s “Typescript Interview” study doc. Examples are checked with the TypeScript compiler in strict mode.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.