Merge two parameter tuples into the argument list that satisfies both, keeping required, optional and rest positions straight while intersecting element types.
IntersectParameters<l, r> takes two parameter tuples and computes the argument list a caller would have to pass to satisfy both of them at once. If one function wants [{ a: 0 }] and another wants [{ b: 1 }], a call that pleases both passes a single object with a and b on it. Libraries that compose middleware or merge two handler signatures into one need exactly this operation.
type Result = IntersectParameters<
[{ a: 0 }, { b: 1 }?, { c: 2 }?, ...{ d: 3 }[]],
[{ e: 4 }?, { f: 5 }?, ...{ g: 6 }[]]
>
type Expected = [
{
a: 0
e: 4
},
{
b: 1
f: 5
}?,
{
c: 2
g: 6
}?,
...{
d: 3
g: 6
}[],
]Writing l & r gets you an intersection of two tuple types, which is not a tuple at all. The real work is that a parameter list has three kinds of positions: required, optional and rest. You have to line the two lists up position by position, decide whether each merged position is still optional, and handle the point where one side has run out of fixed elements while the other keeps going.
Given two parameter arrays, compute a third tuple representing the type of args required to satisfy both of the original tuples.
Your solution should correctly handle fixed and non-fixed length arrays, optional elements and variadic elements. For example:
type Result = IntersectParameters<
[{ a: 0 }, { b: 1 }?, { c: 2 }?, ...{ d: 3 }[]],
[{ e: 4 }?, { f: 5 }?, ...{ g: 6 }[]]
>
type Expected = [
{
a: 0
e: 4
},
{
b: 1
f: 5
}?,
{
c: 2
g: 6
}?,
...{
d: 3
g: 6
}[]
]View on GitHub: https://tsch.js.org/31997
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type IntersectParameters<
l extends readonly unknown[],
r extends readonly unknown[],
> = l & r
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type result1 = IntersectParameters<[], []>
type TwoEmpty = Expect<Equal<[], result1>>
type result2 = IntersectParameters<[], [string, number, ...boolean[]]>
type OneEmpty = Expect<Equal<[string, number, ...boolean[]], result2>>
type result3 = IntersectParameters<['a'], [string, number]>
type LongerParametersPreserved = Expect<Equal<['a', number], resuUnlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type Prettify<t> = { [k in keyof t]: t[k] }
type IsRestOnly<t extends readonly unknown[]> = t[number][] extends t
? true
: false
type Shifted = { head: unknown; tail: readonly unknown[]; required: boolean }
type Shift<t extends readonly unknown[]> = t extends readonly [
infer head,
...infer tail,
]
? { head: head; tail: tail; required: true }
: t extends readonly [(infer head)?, ...infer tail]
? { head: head; tail: tail; required: false }
: never
type Step<l extends Shifted, r extends Shifted> = true extends
| l['required']
| r['required']
? [
Prettify<l['head'] & r['head']>,
...IntersectParameters<l['tail'], r['tail']>,
]
: [
Prettify<l['head'] & r['head']>?,
...IntersectParameters<l['tail'], r['tail']>,
]
type IntersectParameters<
l extends readonly unknown[],
r extends readonly unknown[],
> = l extends []
? r
: r extends []
? l
: [IsRestOnly<l>, IsRestOnly<r>] extends [true, true]
? Prettify<l[number] & r[number]>[]
: Step<Shift<l>, Shift<r>>Shift<t> is the part that understands parameter lists: it reports the element at the front of a tuple, what is left behind it, and whether a caller must actually pass that argument. The first pattern, [infer head, ...infer tail], matches only when the first element is required, because an optional-first tuple like [{ a: 0 }?] has length 0 | 1 and is not assignable to a pattern that demands at least one element. Assignability answers the question, so nothing has to inspect the modifier directly.
type A = Shift<[{ a: 0 }, ...{ b: 1 }[]]> // head { a: 0 }, tail { b: 1 }[], required
type B = Shift<[{ c: 2 }?, ...{ d: 3 }[]]> // head { c: 2 }, tail { d: 3 }[], optional
type C = Shift<{ g: 6 }[]> // head { g: 6 }, tail { g: 6 }[], optionalTwo details carry weight here. Inferring from an optional position yields { c: 2 }, not { c: 2 } | undefined, so optionality stays in the required field instead of leaking into the element type. And a bare array shifts to itself, because a rest position never runs out. That is what lets one side keep feeding elements from its rest while the other works through its fixed part.
{ a: 0 } & { e: 4 } is already the correct type, but not the one the tests ask for. Equal compares structure as the compiler stored it, and a stored intersection is not the same thing as a flat { a: 0; e: 4 }. The homomorphic mapped type Prettify rebuilds the properties and flattens it.
type M = Prettify<{ a: 0 } & { e: 4 }> // { a: 0; e: 4 }
type N = Prettify<'a' & string> // 'a'The second line is the reason Prettify is written as [k in keyof t] over a bare type parameter. That form is homomorphic, and a homomorphic mapped type applied to a primitive hands the primitive back untouched, so 'a' survives the merge instead of turning into an object carrying all of string's methods.
Step decides the modifier. A position is required as soon as either list requires it, which is a membership test on a union of two booleans: true extends true | false holds, true extends false | false does not. The required branch builds [merged, ...rest], the optional branch [merged?, ...rest], with rest the recursive call on both tails. Building the tuple front to back is what makes the optional element legal, since TypeScript rejects an optional element that follows a rest element, and an [...acc, next?] accumulator would not compile.
Two of the three exits are the empty tuple. If l is [], everything still to be passed is whatever r has left, so r comes back untouched. The test is exact: [{ a: 0 }?] has length 0 | 1 and is not assignable to [], so a tuple that could be empty does not take this branch. Handing the remainder back verbatim is also what keeps IntersectParameters<[unknown], []> honest, because merging unknown into a missing element would mean Prettify<unknown>, which evaluates to {}.
The third exit is what keeps the recursion finite. Once both sides are rest-only, Shift returns the same pair of tuples forever and Step would recurse without shrinking anything, so IsRestOnly catches that case first by asking whether a tuple is assignable from an array of its own element type. IsRestOnly<{ c: 2 }[]> is true, while IsRestOnly<[{ a: 0 }?, ...{ d: 3 }[]]> is false, since the fixed part is in the way. When both sides answer true, the result is one array of the merged element type. Every other path consumes at least one fixed element from at least one side, and fixed parts are finite. Walking the kitchen sink from above: position 0 merges required { a: 0 } with optional { e: 4 } and stays required, position 1 merges two optionals, position 2 finds the right list out of fixed elements and merges { c: 2 } with { g: 6 } from its rest, and then both sides are rest-only and the walk ends with ...{ d: 3; g: 6 }[].
[unknown] against [] → [unknown]. The early return for the empty side is what stops unknown from collapsing to {}.[('a' | 'b' | 'c')?] against [string, 1 | 2 | 3] → ['a' | 'b' | 'c', 1 | 2 | 3]. The right side requires the first argument, so the merged position loses its optional modifier.[{ a: 0 }, { b: 1 }] against { c: 2 }[] → [{ a: 0; c: 2 }, { b: 1; c: 2 }, ...{ c: 2 }[]]. The self-shifting rest feeds both fixed positions and is still there at the end.['a', 'b'] against [string, ...string[]] → ['a', 'b', ...string[]]. The variadic tail outlives the fixed side and comes through unmerged.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges