Build a generic VoidFunction type that takes its parameter list from a tuple. The trap: a naked conditional distributes over boolean and splits the result in two.
TypeScript already has a VoidFunction type, and it is stuck at zero parameters.
This challenge replaces it with a generic one. VoidFunction<T> produces a function type that returns void and takes its parameter list from T: pass a tuple and every element becomes a parameter, pass a single type and it becomes the only parameter, pass nothing and you get a function that takes no arguments. Two short branches cover all of that, and almost everything interesting happens in how the conditional between them reacts to a union.
For example
VoidFunction // () => void
VoidFunction<boolean> // (arg: boolean) => void
VoidFunction<[boolean, boolean]> // (...args: [boolean, boolean]) => voidThe built-in VoidFunction type does not accept a type parameter. Implement a
generic VoidFunction type that does.
VoidFunction // () => void
VoidFunction<boolean> // (arg: boolean) => void
VoidFunction<[boolean, boolean]> // (...args: [boolean, boolean]) => voidView on GitHub: https://tsch.js.org/9535
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type VoidFunction<T> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type cases = [
Expect<Equal<VoidFunction, () => void>>,
Expect<Equal<VoidFunction<boolean>, (arg: boolean) => void>>,
Expect<Equal<VoidFunction<[boolean, boolean]>, (arg1: boolean, arg2: boolean) => void>>,
Expect<Equal<VoidFunction<[boolean, boolean, boolean]>, (...args: [boolean, boolean, boolean]) => void>>,
]
Get access to all 200+ challenges, including every medium, hard, and extreme one.
Monthly subscription.
Cancel anytime + 30-day money-back guarantee.
The solution in full:
type VoidFunction<T = []> = [T] extends [unknown[]]
? (...args: T) => void
: (arg: T) => voidThree moving parts: the default on T, the tuple spread into a rest parameter, and the square brackets wrapped around the conditional.
A rest parameter can be typed by a tuple, and when it is, the tuple's elements become the function's parameters one at a time:
type Two = (...args: [boolean, string]) => void
// the same type as (arg1: boolean, arg2: string) => voidThese two spellings are not merely assignable to each other, they are the same type to the compiler. That matters here, because the tests compare VoidFunction<[boolean, boolean]> against the hand-written (arg1: boolean, arg2: boolean) => void using Equal, which only accepts identical types.
So the tuple case needs no computation at all. Hand T to a rest parameter and you are done.
The other case is VoidFunction<boolean>, which has to become (arg: boolean) => void. A non-tuple T is wrapped as a single parameter instead of spread, so a conditional type picks between the two shapes:
type Naive<T = []> = T extends unknown[]
? (...args: T) => void
: (arg: T) => voidThat reads right, passes three of the four tests, and fails on boolean.
boolean is not a primitive as far as the type system is concerned. It is the union true | false. When a union reaches a conditional type in the naked position, meaning the checked type is the bare type parameter and nothing else, TypeScript splits the union apart, evaluates the conditional once per member, and unions the results back together. That behaviour is called distribution.
type Result = Naive<boolean>
// Naive<true> | Naive<false>
// ((arg: true) => void) | ((arg: false) => void)A union of two narrow function types is not the same type as one function taking boolean, so Equal rejects it. The branches were never wrong; the conditional simply never saw boolean in one piece.
Distribution only kicks in when the checked type is a bare type parameter. Wrap it in a one-element tuple and that condition no longer holds:
type Guard<T = []> = [T] extends [unknown[]]
? (...args: T) => void
: (arg: T) => voidThe wrapper has to go on both sides, otherwise you would be asking whether [T] is an array, which it always is. With [T] extends [unknown[]], boolean arrives in the false branch intact and T inside the branches is still plain boolean, not [boolean]. The brackets exist for the comparison only.
The first test writes VoidFunction with no type argument, so T needs a default. The starter stub has none, which is why the editor greets you with "Generic type 'VoidFunction' requires 1 type argument(s)" before any assertion has even run.
T = [] is the value that works. An empty tuple takes the array branch and gives (...args: []) => void, and a rest parameter over an empty tuple contributes no parameters, so the result is () => void.
VoidFunction with no argument → () => void, via the [] default collapsing to an empty parameter list.VoidFunction<boolean> → (arg: boolean) => void, the case that fails without the bracket guard.VoidFunction<[boolean, boolean]> is compared against (arg1: boolean, arg2: boolean) => void, so the rest-parameter form has to be identical to a written-out parameter list, not just compatible with it.VoidFunction<[boolean, boolean, boolean]> is compared against the rest-parameter spelling instead, checking the same identity from the other side.One case the tests leave out is an unbounded array: VoidFunction<string[]> takes the array branch too and yields (...args: string[]) => void, a function of any number of strings. The solution handles it for free, since string[] matches unknown[] just as a tuple does.
This challenge is originally from here.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges