#9535•Medium

Void Function

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]) => void

Challenge Instructions: Void Function

Medium

The 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]) => void

View on GitHub: https://tsch.js.org/9535

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ 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>>,
]

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

Monthly subscription.
Cancel anytime + 30-day money-back guarantee.

Detailed Explanation

The solution in full:

type VoidFunction<T = []> = [T] extends [unknown[]]
  ? (...args: T) => void
  : (arg: T) => void

Three moving parts: the default on T, the tuple spread into a rest parameter, and the square brackets wrapped around the conditional.

A tuple as a parameter list

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) => void

These 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 naive conditional

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) => void

That reads right, passes three of the four tests, and fails on boolean.

Why boolean breaks it

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.

Turning distribution off

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) => void

The 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 default

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.

Edge cases the tests cover

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.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges