When recursively destructuring array types, at some point the compiler discards the tuple information. After the first recursion, the literal tuples from the input value are treated as plain arrays and [infer H, ...infer T] doesn't match anymore.
Below is a minimal example using a query DSL. Why does this happen, and is it possible to preserve the tuple type information to make the example work?
type Conjunctions<A, C> = C extends [infer H, ...infer T]
? [Query<A, H>, ...Conjunctions<A, T>]
: [];
type Query<A, Q> = {
[K in keyof Q]: K extends 'and' | 'or'
? Q[K] extends [infer H, ...infer T]
? Conjunctions<A, [H, ...T]>
: { error: 'Invalid conjunction'; type: Q[K] }
: K extends keyof A
? A[K]
: { error: `Invalid key ${K & string}` };
};
const query =
<A>() =>
<B extends Query<A, B>>(_: B) =>
undefined;
type Person =
| { name: 'Nobby Nobbs'; age: 34 } //
| { name: 'Fred Colon'; age: 58 };
query<Person>()({
or: [
{
name: 'Nobby Nobbs',
and: [{ name: 'Fred Colon', age: 58 }],
},
],
});
The compiler accepts the or conjunction but complains about the nested and conjunction:
TS2739: Type { name: string; age: number; }[] is missing the following properties from type
{
error: 'Invalid conjunction';
type: {
name: string;
age: number;
}[];
}
: error, typ
TypeScript version: 5.9.3
[{ name: 'Fred Colon', age: 58 }] is defined as { name: string, age: number }[], which is how type inference works based on the argument values when calling a generic function (the type is sometimes broader).
You can specify const for the parameter,
<const B extends Query<A, B>>(_: B) =>
then the type will be exactly equal to the value passed. This results in ReadonlyArray instead of Array, which are a supertype of Array (a ReadonlyArray has no methods that modify its contents, such as push, pop, etc.), and this must be taken into account in the types Conjunctions and Query:
...
Q[K] extends readonly [infer H, ...infer T]
...
Full code: https://tsplay.dev/mAX64N