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> // falseImplement 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> // falseView on GitHub: https://tsch.js.org/37814
Change the following code to make the test cases pass (no type check errors).
/* _____________ 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>>,
]
Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type IsBooleanLiteral<T extends boolean> = [T] extends [true]
? true
: [T] extends [false]
? true
: falseFour lines, and the brackets are doing all the work. Here is why they are needed.
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 wrongNaive<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 -> trueBoth 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.
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.
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
// -> falseOnly 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.
IsBooleanLiteral<true> and IsBooleanLiteral<false> both return true. Two separate checks are needed because [false] extends [true] is false, so a single comparison against one literal cannot cover both.IsBooleanLiteral<boolean> returns false. This is the case the naive version gets wrong, and the only reason the tuples are there.true or false, not boolean. If a conditional type distributes and its branches disagree, the union of the results is true | false, which collapses to boolean and fails the Equal assertion.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