#37814Medium

IsBooleanLiteral

Tell true and false apart from boolean itself. A conditional type that distributes over the union returns the wrong answer, and tuple wrapping stops it.

IsBooleanLiteral<T> answers one question: is this type a single boolean literal, or is it boolean? It returns true for true and for false, and false for boolean itself.

The catch is that boolean is not a primitive type in the way string or number are. Internally TypeScript models it as the union true | false, and unions behave in a special way inside conditional types. That behaviour is what turns a one-line check into a real challenge.

type Case1 = IsBooleanLiteral<true>   // true
type Case2 = IsBooleanLiteral<false>  // true
type Case3 = IsBooleanLiteral<boolean> // false

Challenge Instructions: IsBooleanLiteral

Medium

Implement a utility type IsBooleanLiteral<T> that takes a type T and returns true if T is strictly true or false. If T is the union type boolean (which represents true | false), it should return false.

For example:

type Case1 = IsBooleanLiteral<true>   // true
type Case2 = IsBooleanLiteral<false>  // true
type Case3 = IsBooleanLiteral<boolean> // false

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

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

ChallengeSolution
/* _____________ Your Code Here _____________ */

type IsBooleanLiteral<T extends boolean> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type cases = [
  Expect<Equal<IsBooleanLiteral<true>, true>>,
  Expect<Equal<IsBooleanLiteral<false>, true>>,
  Expect<Equal<IsBooleanLiteral<boolean>, false>>,
]

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The solution in full:

type IsBooleanLiteral<T extends boolean> = [T] extends [true]
  ? true
  : [T] extends [false]
    ? true
    : false

Four lines, and the brackets are doing all the work. Here is why they are needed.

The naive attempt, and why it fails

The obvious reading of the task is "is T one of the two literals":

type Naive<T extends boolean> = T extends true | false ? true : false
 
type A = Naive<true>    // true
type B = Naive<boolean> // true, and that is wrong

Naive<boolean> should be false, but it comes back true. The reason is distribution. When the type being checked in a conditional type is a bare generic parameter, and the type passed in is a union, TypeScript does not test the union as a whole. It splits the union apart, runs the conditional once per member, and unions the results back together.

So Naive<boolean> is evaluated as Naive<true> | Naive<false>:

// step 1: boolean is true | false, so the conditional splits
//   true  extends true | false ? true : false  ->  true
//   false extends true | false ? true : false  ->  true
// step 2: union the branch results
//   true | true  ->  true

Both members are literals, both branches return true, and the union of true with true collapses back to plain true. The union never gets a chance to be seen as a union, because it was taken apart before the check ran.

Turning distribution off

Distribution only happens for a naked type parameter. If you wrap both sides of extends in a one element tuple, T is no longer naked, and TypeScript compares the tuples as whole types:

type IsTrue<T extends boolean> = [T] extends [true] ? true : false
 
type C = IsTrue<true>    // true
type D = IsTrue<boolean> // false

[boolean] is [true | false], a tuple whose single element can be either literal. Is it assignable to [true]? No, because the element type true | false is wider than true. The check fails as a whole, with no splitting, which is exactly what we want.

Any wrapper works for this, including a function type or an object type. The tuple is just the cheapest one to read and to compile.

Putting the two checks together

With distribution disabled, T is a literal exactly when it matches [true] or matches [false]:

// T = true     ->  [true] extends [true]     ->  true
// T = false    ->  [false] extends [true]    ->  no
//                  [false] extends [false]   ->  true
// T = boolean  ->  [boolean] extends [true]  ->  no
//                  [boolean] extends [false] ->  no
//                                            ->  false

Only boolean reaches the final false, and that is the whole type.

There is a shorter formulation that trades the tuples for a reversed assignability check:

[object Object]

Read it as "is T wide enough to hold all of boolean". Under the constraint T extends boolean, only boolean itself is that wide, so the two versions agree on every input the tests use. The bracketed version is worth knowing anyway, because the distribution problem it solves shows up in far more places than this one challenge.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

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

Or start solving right away: explore all TypeScript challenges